1
This commit is contained in:
+9
@@ -0,0 +1,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-present alipay.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
# @rc-component/select
|
||||
|
||||
---
|
||||
|
||||
React Select Component.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
[![NPM version][npm-image]][npm-url]
|
||||
[![npm download][download-image]][download-url]
|
||||
[![build status][github-actions-image]][github-actions-url]
|
||||
[![Codecov][codecov-image]][codecov-url]
|
||||
[![bundle size][bundlephobia-image]][bundlephobia-url]
|
||||
[![dumi][dumi-image]][dumi-url]
|
||||
|
||||
[npm-image]: https://img.shields.io/npm/v/@rc-component/select.svg?style=flat-square
|
||||
[npm-url]: http://npmjs.org/package/@rc-component/select
|
||||
[github-actions-image]: https://github.com/react-component/select/actions/workflows/test.yml/badge.svg
|
||||
[github-actions-url]: https://github.com/react-component/select/actions/workflows/test.yml
|
||||
[codecov-image]: https://img.shields.io/codecov/c/github/react-component/select/master.svg?style=flat-square
|
||||
[codecov-url]: https://app.codecov.io/gh/react-component/select
|
||||
[david-url]: https://david-dm.org/react-component/select
|
||||
[david-image]: https://david-dm.org/react-component/select/status.svg?style=flat-square
|
||||
[david-dev-url]: https://david-dm.org/react-component/select?type=dev
|
||||
[david-dev-image]: https://david-dm.org/react-component/select/dev-status.svg?style=flat-square
|
||||
[download-image]: https://img.shields.io/npm/dm/@rc-component/select.svg?style=flat-square
|
||||
[download-url]: https://npmjs.org/package/@rc-component/select
|
||||
[bundlephobia-url]: https://bundlephobia.com/package/@rc-component/select
|
||||
[bundlephobia-image]: https://badgen.net/bundlephobia/minzip/@rc-component/select
|
||||
[dumi-url]: https://github.com/umijs/dumi
|
||||
[dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square
|
||||
|
||||
## Screenshots
|
||||
|
||||
<img src="https://gw.alipayobjects.com/zos/antfincdn/d13eUZlgdJ/tupian.png" />
|
||||
|
||||
## Feature
|
||||
|
||||
- support IE11+,Chrome,Firefox,Safari
|
||||
|
||||
### Keyboard
|
||||
|
||||
- Open select (focus input || focus and click)
|
||||
- KeyDown/KeyUp/Enter to navigate menu
|
||||
|
||||
## install
|
||||
|
||||
[](https://npmjs.org/package/@rc-component/select)
|
||||
|
||||
## Usage
|
||||
|
||||
### basic use
|
||||
|
||||
```jsx | pure
|
||||
import Select, { Option } from '@rc-component/select';
|
||||
import '@rc-component/select/assets/index.css';
|
||||
|
||||
export default () => (
|
||||
<Select>
|
||||
<Option value="jack">jack</Option>
|
||||
<Option value="lucy">lucy</Option>
|
||||
<Option value="yiminghe">yiminghe</Option>
|
||||
</Select>
|
||||
);
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### Select props
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
| name | description | type | default |
|
||||
| --- | --- | --- | --- |
|
||||
| id | html id to set on the component wrapper | String | '' |
|
||||
| className | additional css class of root dom node | String | '' |
|
||||
| data-\* | html data attributes to set on the component wrapper | String | '' |
|
||||
| prefixCls | prefix class | String | '' |
|
||||
| animation | dropdown animation name. only support slide-up now | String | '' |
|
||||
| transitionName | dropdown css animation name | String | '' |
|
||||
| choiceTransitionName | css animation name for selected items at multiple mode | String | '' |
|
||||
| dropdownMatchSelectWidth | whether dropdown's width is same with select | boolean | true |
|
||||
| dropdownClassName | additional className applied to dropdown | String | - |
|
||||
| dropdownStyle | additional style applied to dropdown | React.CSSProperties | {} |
|
||||
| dropdownAlign | additional align applied to dropdown | [AlignType](https://github.com/react-component/trigger/blob/728d7e92394aa4b3214650f743fc47e1382dfa68/src/interface.ts#L25-L80) | {} |
|
||||
| dropdownMenuStyle | additional style applied to dropdown menu | Object | React.CSSProperties |
|
||||
| notFoundContent | specify content to show when no result matches. | ReactNode | 'Not Found' |
|
||||
| tokenSeparators | separator used to tokenize on tag/multiple mode | string[]? | |
|
||||
| open | control select open | boolean | |
|
||||
| defaultOpen | control select default open | boolean | |
|
||||
| placeholder | select placeholder | React Node | |
|
||||
| showSearch | whether show search input in single mode | boolean \| Object | true |
|
||||
| allowClear | whether allowClear | boolean | { clearIcon?: ReactNode } | false |
|
||||
| tags | when tagging is enabled the user can select from pre-existing options or create a new tag by picking the first choice, which is what the user has typed into the search box so far. | boolean | false |
|
||||
| tagRender | render custom tags. | (props: CustomTagProps) => ReactNode | - |
|
||||
| maxTagTextLength | max tag text length to show | number | - |
|
||||
| maxTagCount | max tag count to show | number | - |
|
||||
| maxTagPlaceholder | placeholder for omitted values | ReactNode/function(omittedValues) | - |
|
||||
| combobox | enable combobox mode(can not set multiple at the same time) | boolean | false |
|
||||
| multiple | whether multiple select | boolean | false |
|
||||
| disabled | whether disabled select | boolean | false |
|
||||
| optionLabelProp | render option value or option children as content of select | String: 'value'/'children' | 'value' |
|
||||
| defaultValue | initial selected option(s) | String \| String[] | - |
|
||||
| value | current selected option(s) | String \| String[] \| {key:String, label:React.Node} \| {key:String, label:React.Node}[] | - |
|
||||
| labelInValue | whether to embed label in value, see above value type. Not support `combobox` mode | boolean | false |
|
||||
| backfill | whether backfill select option to search input (Only works in single and combobox mode) | boolean | false |
|
||||
| onChange | called when select an option or input value change(combobox) | function(value, option:Option \| Option[]) | - |
|
||||
| onBlur | called when blur | function | - |
|
||||
| onFocus | called when focus | function | - |
|
||||
| onPopupScroll | called when menu is scrolled | function | - |
|
||||
| onSelect | called when a option is selected. param is option's value and option instance | Function(value, option:Option) | - |
|
||||
| onDeselect | called when a option is deselected. param is option's value. only called for multiple or tags | Function(value, option:Option) | - |
|
||||
| onInputKeyDown | called when key down on input | Function(event) | - |
|
||||
| defaultActiveFirstOption | whether active first option by default | boolean | true |
|
||||
| getPopupContainer | container which popup select menu rendered into | function(trigger:Node):Node | function(){return document.body;} |
|
||||
| getInputElement | customize input element | function(): Element | - |
|
||||
| showAction | actions trigger the dropdown to show | String[]? | - |
|
||||
| autoFocus | focus select after mount | boolean | - |
|
||||
| prefix | specify the select prefix icon or text | ReactNode | - |
|
||||
| suffixIcon | specify the select arrow icon | ReactNode | - |
|
||||
| clearIcon | specify the clear icon | ReactNode | - |
|
||||
| removeIcon | specify the remove icon | ReactNode | - |
|
||||
| menuItemSelectedIcon | specify the item selected icon | ReactNode \| (props: MenuItemProps) => ReactNode | - |
|
||||
| dropdownRender | render custom dropdown menu | (menu: React.Node) => ReactNode | - |
|
||||
| loading | show loading icon in arrow | boolean | false |
|
||||
| virtual | Disable virtual scroll | boolean | true |
|
||||
| direction | direction of dropdown | 'ltr' \| 'rtl' | 'ltr' |
|
||||
| optionRender | Custom rendering options | (oriOption: FlattenOptionData\<BaseOptionType\> , info: { index: number }) => React.ReactNode | - |
|
||||
| labelRender | Custom rendering label | (props: LabelInValueType) => React.ReactNode | - |
|
||||
| maxCount | The max number of items can be selected | number | - |
|
||||
|
||||
### Methods
|
||||
|
||||
| name | description | parameters | return |
|
||||
| ----- | ------------------------- | ---------- | ------ |
|
||||
| focus | focus select programmably | - | - |
|
||||
| blur | blur select programmably | - | - |
|
||||
|
||||
### showSearch
|
||||
|
||||
| name | description | type | default |
|
||||
| --- | --- | --- | --- |
|
||||
| autoClearSearchValue | auto clear search input value when multiple select is selected/deselected | boolean | true |
|
||||
| filterOption | whether filter options by input value. default filter by option's optionFilterProp prop's value | boolean\| (inputValue: string, option: Option) => boolean | true |
|
||||
| filterSort | Sort function for search options sorting, see [Array.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort)'s compareFunction. | Function(optionA:Option, optionB: Option) | - |
|
||||
| optionFilterProp | which prop value of option will be used for filter if filterOption is true | String | 'value' |
|
||||
| searchValue | The current input "search" text | string | - |
|
||||
| onSearch | called when input changed | function | - |
|
||||
|
||||
### Option props
|
||||
|
||||
| name | description | type | default |
|
||||
| --- | --- | --- | --- |
|
||||
| className | additional class to option | String | '' |
|
||||
| disabled | no effect for click or keydown for this item | boolean | false |
|
||||
| key | if react want you to set key, then key is same as value, you can omit value | String/number | - |
|
||||
| value | default filter by this attribute. if react want you to set key, then key is same as value, you can omit value | String/number | - |
|
||||
| title | if you are not satisfied with auto-generated `title` which is show while hovering on selected value, you can customize it with this property | String | - |
|
||||
|
||||
### OptGroup props
|
||||
|
||||
| name | description | type | default |
|
||||
| --- | --- | --- | --- |
|
||||
| label | group label | String/React.Element | - |
|
||||
| key | - | String | - |
|
||||
| value | default filter by this attribute. if react want you to set key, then key is same as value, you can omit value | String | - |
|
||||
| className | same as `Option props` | String | '' |
|
||||
| title | same as `Option props` | String | - |
|
||||
|
||||
## Development
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
local example: http://localhost:9001/
|
||||
|
||||
online example: https://select-react-component.vercel.app/
|
||||
|
||||
## Test Case
|
||||
|
||||
```
|
||||
npm test
|
||||
```
|
||||
|
||||
## Coverage
|
||||
|
||||
```
|
||||
npm run coverage
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
@rc-component/select is released under the MIT license.
|
||||
+375
@@ -0,0 +1,375 @@
|
||||
.rc-select.rc-select {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
border: 1px solid blue;
|
||||
position: relative;
|
||||
}
|
||||
.rc-select.rc-select .rc-select-content {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* Prevent content from wrapping */
|
||||
min-width: 0;
|
||||
/* allow flex item to shrink */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
position: relative;
|
||||
}
|
||||
.rc-select.rc-select .rc-select-input {
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
.rc-select.rc-select .rc-select-placeholder {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.rc-select.rc-select .rc-select-placeholder::after {
|
||||
content: '\00a0';
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rc-select.rc-select .rc-select-content,
|
||||
.rc-select.rc-select .rc-select-input,
|
||||
.rc-select.rc-select .rc-select-placeholder {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.rc-select.rc-select .rc-select-prefix,
|
||||
.rc-select.rc-select .rc-select-suffix,
|
||||
.rc-select.rc-select .rc-select-clear {
|
||||
flex: none;
|
||||
}
|
||||
.rc-select.rc-select .rc-select-clear {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
.rc-select.rc-select-single .rc-select-input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
.rc-select.rc-select-multiple .rc-select-selection-item {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.rc-select.rc-select-multiple .rc-select-placeholder {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.rc-select.rc-select-multiple .rc-select-input {
|
||||
width: calc(var(--select-input-width, 10) * 1px);
|
||||
min-width: 4px;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.rc-select {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
width: 100px;
|
||||
position: relative;
|
||||
}
|
||||
.rc-select-disabled,
|
||||
.rc-select-disabled input {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.rc-select-disabled .rc-select-selector {
|
||||
opacity: 0.3;
|
||||
}
|
||||
.rc-select-show-arrow.rc-select-loading .rc-select-arrow-icon::after {
|
||||
box-sizing: border-box;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 100%;
|
||||
border: 2px solid #999;
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
transform: none;
|
||||
margin-top: 4px;
|
||||
animation: rcSelectLoadingIcon 0.5s infinite;
|
||||
}
|
||||
.rc-select .rc-select-selection-placeholder {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
.rc-select .rc-select-selection-search-input {
|
||||
appearance: none;
|
||||
}
|
||||
.rc-select .rc-select-selection-search-input::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
appearance: none;
|
||||
}
|
||||
.rc-select-single .rc-select-selector {
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
.rc-select-single .rc-select-selector .rc-select-selection-wrap {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.rc-select-single .rc-select-selector .rc-select-selection-search {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.rc-select-single .rc-select-selector .rc-select-selection-search-input {
|
||||
width: 100%;
|
||||
}
|
||||
.rc-select-single .rc-select-selector .rc-select-selection-item,
|
||||
.rc-select-single .rc-select-selector .rc-select-selection-placeholder {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 3px;
|
||||
pointer-events: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
.rc-select-single:not(.rc-select-customize-input) .rc-select-selector {
|
||||
padding: 1px;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
.rc-select-single:not(.rc-select-customize-input) .rc-select-selector .rc-select-selection-search-input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: rgba(255, 0, 0, 0.2);
|
||||
width: 100%;
|
||||
}
|
||||
.rc-select-multiple .rc-select-selector {
|
||||
display: flex;
|
||||
padding: 1px;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
.rc-select-multiple .rc-select-selector .rc-select-selection-item {
|
||||
flex: none;
|
||||
background: #bbb;
|
||||
border-radius: 4px;
|
||||
margin-right: 2px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
.rc-select-multiple .rc-select-selector .rc-select-selection-item-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.rc-select-multiple .rc-select-selector .rc-select-selection-overflow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.rc-select-multiple .rc-select-selector .rc-select-selection-overflow-item {
|
||||
flex: none;
|
||||
max-width: 100%;
|
||||
}
|
||||
.rc-select-multiple .rc-select-selector .rc-select-selection-search {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
}
|
||||
.rc-select-multiple .rc-select-selector .rc-select-selection-search-input,
|
||||
.rc-select-multiple .rc-select-selector .rc-select-selection-search-mirror {
|
||||
padding: 1px;
|
||||
font-family: system-ui;
|
||||
}
|
||||
.rc-select-multiple .rc-select-selector .rc-select-selection-search-mirror {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
white-space: nowrap;
|
||||
position: none;
|
||||
left: 0;
|
||||
top: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.rc-select-multiple .rc-select-selector .rc-select-selection-search-input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: rgba(255, 0, 0, 0.2);
|
||||
width: 100%;
|
||||
}
|
||||
.rc-select-allow-clear.rc-select-multiple .rc-select-selector {
|
||||
padding-right: 20px;
|
||||
}
|
||||
.rc-select-allow-clear .rc-select-clear {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 0;
|
||||
}
|
||||
.rc-select-show-arrow.rc-select-multiple .rc-select-selector {
|
||||
padding-right: 20px;
|
||||
}
|
||||
.rc-select-show-arrow .rc-select-arrow {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 0;
|
||||
}
|
||||
.rc-select-show-arrow .rc-select-arrow-icon::after {
|
||||
content: '';
|
||||
border: 5px solid transparent;
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: inline-block;
|
||||
border-top-color: #999;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
.rc-select-focused .rc-select-selector {
|
||||
border-color: blue !important;
|
||||
}
|
||||
.rc-select-dropdown {
|
||||
border: 1px solid green;
|
||||
min-height: 100px;
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
}
|
||||
.rc-select-dropdown-hidden {
|
||||
display: none;
|
||||
}
|
||||
.rc-select-item {
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
padding: 4px 16px;
|
||||
}
|
||||
.rc-select-item-group {
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
font-size: 80%;
|
||||
}
|
||||
.rc-select-item-option {
|
||||
position: relative;
|
||||
}
|
||||
.rc-select-item-option-grouped {
|
||||
padding-left: 24px;
|
||||
}
|
||||
.rc-select-item-option .rc-select-item-option-state {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.rc-select-item-option-active {
|
||||
background: #ddd;
|
||||
}
|
||||
.rc-select-item-option-disabled {
|
||||
color: #999;
|
||||
}
|
||||
.rc-select-item-empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
.rc-select-selection__choice-zoom {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.rc-select-selection__choice-zoom-appear {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
.rc-select-selection__choice-zoom-appear.rc-select-selection__choice-zoom-appear-active {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
.rc-select-selection__choice-zoom-leave {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
.rc-select-selection__choice-zoom-leave.rc-select-selection__choice-zoom-leave-active {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
.rc-select-dropdown-slide-up-enter,
|
||||
.rc-select-dropdown-slide-up-appear {
|
||||
animation-duration: 0.3s;
|
||||
animation-fill-mode: both;
|
||||
transform-origin: 0 0;
|
||||
opacity: 0;
|
||||
animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1);
|
||||
animation-play-state: paused;
|
||||
}
|
||||
.rc-select-dropdown-slide-up-leave {
|
||||
animation-duration: 0.3s;
|
||||
animation-fill-mode: both;
|
||||
transform-origin: 0 0;
|
||||
opacity: 1;
|
||||
animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34);
|
||||
animation-play-state: paused;
|
||||
}
|
||||
.rc-select-dropdown-slide-up-enter.rc-select-dropdown-slide-up-enter-active.rc-select-dropdown-placement-bottomLeft,
|
||||
.rc-select-dropdown-slide-up-appear.rc-select-dropdown-slide-up-appear-active.rc-select-dropdown-placement-bottomLeft,
|
||||
.rc-select-dropdown-slide-up-enter.rc-select-dropdown-slide-up-enter-active.rc-select-dropdown-placement-bottomRight,
|
||||
.rc-select-dropdown-slide-up-appear.rc-select-dropdown-slide-up-appear-active.rc-select-dropdown-placement-bottomRight {
|
||||
animation-name: rcSelectDropdownSlideUpIn;
|
||||
animation-play-state: running;
|
||||
}
|
||||
.rc-select-dropdown-slide-up-leave.rc-select-dropdown-slide-up-leave-active.rc-select-dropdown-placement-bottomLeft,
|
||||
.rc-select-dropdown-slide-up-leave.rc-select-dropdown-slide-up-leave-active.rc-select-dropdown-placement-bottomRight {
|
||||
animation-name: rcSelectDropdownSlideUpOut;
|
||||
animation-play-state: running;
|
||||
}
|
||||
.rc-select-dropdown-slide-up-enter.rc-select-dropdown-slide-up-enter-active.rc-select-dropdown-placement-topLeft,
|
||||
.rc-select-dropdown-slide-up-appear.rc-select-dropdown-slide-up-appear-active.rc-select-dropdown-placement-topLeft,
|
||||
.rc-select-dropdown-slide-up-enter.rc-select-dropdown-slide-up-enter-active.rc-select-dropdown-placement-topRight,
|
||||
.rc-select-dropdown-slide-up-appear.rc-select-dropdown-slide-up-appear-active.rc-select-dropdown-placement-topRight {
|
||||
animation-name: rcSelectDropdownSlideDownIn;
|
||||
animation-play-state: running;
|
||||
}
|
||||
.rc-select-dropdown-slide-up-leave.rc-select-dropdown-slide-up-leave-active.rc-select-dropdown-placement-topLeft,
|
||||
.rc-select-dropdown-slide-up-leave.rc-select-dropdown-slide-up-leave-active.rc-select-dropdown-placement-topRight {
|
||||
animation-name: rcSelectDropdownSlideDownOut;
|
||||
animation-play-state: running;
|
||||
}
|
||||
@keyframes rcSelectDropdownSlideUpIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform-origin: 0% 0%;
|
||||
transform: scaleY(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform-origin: 0% 0%;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
@keyframes rcSelectDropdownSlideUpOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform-origin: 0% 0%;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform-origin: 0% 0%;
|
||||
transform: scaleY(0);
|
||||
}
|
||||
}
|
||||
@keyframes rcSelectDropdownSlideDownIn {
|
||||
0% {
|
||||
transform: scaleY(0);
|
||||
transform-origin: 100% 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
transform-origin: 100% 100%;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes rcSelectDropdownSlideDownOut {
|
||||
0% {
|
||||
transform: scaleY(1);
|
||||
transform-origin: 100% 100%;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(0);
|
||||
transform-origin: 100% 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes rcSelectLoadingIcon {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
@select-prefix: ~'rc-select';
|
||||
@import url('./patch.less');
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-input-without-border() {
|
||||
.@{select-prefix}-selection-search-input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: rgba(255, 0, 0, 0.2);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.@{select-prefix} {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
width: 100px;
|
||||
position: relative;
|
||||
|
||||
&-disabled {
|
||||
&,
|
||||
& input {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.@{select-prefix}-selector {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
&-show-arrow&-loading {
|
||||
.@{select-prefix}-arrow {
|
||||
&-icon::after {
|
||||
box-sizing: border-box;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 100%;
|
||||
border: 2px solid #999;
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
transform: none;
|
||||
margin-top: 4px;
|
||||
|
||||
animation: rcSelectLoadingIcon 0.5s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Selector ===============
|
||||
.@{select-prefix}-selection-placeholder {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// ============== Search ===============
|
||||
.@{select-prefix}-selection-search-input {
|
||||
appearance: none;
|
||||
|
||||
&::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
appearance: none;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------- Single ----------------
|
||||
&-single {
|
||||
.@{select-prefix}-selector {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.@{select-prefix}-selection-wrap {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.@{select-prefix}-selection-search {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
&-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.@{select-prefix}-selection-item,
|
||||
.@{select-prefix}-selection-placeholder {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 3px;
|
||||
pointer-events: none;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
// Not customize
|
||||
&:not(.@{select-prefix}-customize-input) {
|
||||
.@{select-prefix}-selector {
|
||||
padding: 1px;
|
||||
border: 1px solid #000;
|
||||
|
||||
.search-input-without-border();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------- Multiple ---------------
|
||||
&-multiple .@{select-prefix}-selector {
|
||||
display: flex;
|
||||
padding: 1px;
|
||||
border: 1px solid #000;
|
||||
|
||||
.@{select-prefix}-selection-item {
|
||||
flex: none;
|
||||
background: #bbb;
|
||||
border-radius: 4px;
|
||||
margin-right: 2px;
|
||||
padding: 0 8px;
|
||||
|
||||
&-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.@{select-prefix}-selection-overflow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&-item {
|
||||
flex: none;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.@{select-prefix}-selection-search {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
|
||||
&-input,
|
||||
&-mirror {
|
||||
padding: 1px;
|
||||
font-family: system-ui;
|
||||
}
|
||||
|
||||
&-mirror {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
white-space: nowrap;
|
||||
position: none;
|
||||
left: 0;
|
||||
top: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input-without-border();
|
||||
}
|
||||
|
||||
// ================ Icons ================
|
||||
&-allow-clear {
|
||||
&.@{select-prefix}-multiple .@{select-prefix}-selector {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.@{select-prefix}-clear {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-show-arrow {
|
||||
&.@{select-prefix}-multiple .@{select-prefix}-selector {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.@{select-prefix}-arrow {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 0;
|
||||
|
||||
&-icon::after {
|
||||
content: '';
|
||||
border: 5px solid transparent;
|
||||
width: 0;
|
||||
height: 0;
|
||||
display: inline-block;
|
||||
border-top-color: #999;
|
||||
transform: translateY(5px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============== Focused ===============
|
||||
&-focused {
|
||||
.@{select-prefix}-selector {
|
||||
border-color: blue !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ============== Dropdown ===============
|
||||
&-dropdown {
|
||||
border: 1px solid green;
|
||||
min-height: 100px;
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
|
||||
&-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// =============== Option ================
|
||||
&-item {
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
padding: 4px 16px;
|
||||
|
||||
// >>> Group
|
||||
&-group {
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
// >>> Option
|
||||
&-option {
|
||||
position: relative;
|
||||
|
||||
&-grouped {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.@{select-prefix}-item-option-state {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 4px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// ------- Active -------
|
||||
&-active {
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
// ------ Disabled ------
|
||||
&-disabled {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// >>> Empty
|
||||
&-empty {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{select-prefix}-selection__choice-zoom {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.@{select-prefix}-selection__choice-zoom-appear {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
|
||||
&&-active {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
.@{select-prefix}-selection__choice-zoom-leave {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
|
||||
&&-active {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.effect() {
|
||||
animation-duration: 0.3s;
|
||||
animation-fill-mode: both;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.@{select-prefix}-dropdown {
|
||||
&-slide-up-enter,
|
||||
&-slide-up-appear {
|
||||
.effect();
|
||||
opacity: 0;
|
||||
animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1);
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
&-slide-up-leave {
|
||||
.effect();
|
||||
opacity: 1;
|
||||
animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34);
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
&-slide-up-enter&-slide-up-enter-active&-placement-bottomLeft,
|
||||
&-slide-up-appear&-slide-up-appear-active&-placement-bottomLeft,
|
||||
&-slide-up-enter&-slide-up-enter-active&-placement-bottomRight,
|
||||
&-slide-up-appear&-slide-up-appear-active&-placement-bottomRight {
|
||||
animation-name: rcSelectDropdownSlideUpIn;
|
||||
animation-play-state: running;
|
||||
}
|
||||
|
||||
&-slide-up-leave&-slide-up-leave-active&-placement-bottomLeft,
|
||||
&-slide-up-leave&-slide-up-leave-active&-placement-bottomRight {
|
||||
animation-name: rcSelectDropdownSlideUpOut;
|
||||
animation-play-state: running;
|
||||
}
|
||||
|
||||
&-slide-up-enter&-slide-up-enter-active&-placement-topLeft,
|
||||
&-slide-up-appear&-slide-up-appear-active&-placement-topLeft,
|
||||
&-slide-up-enter&-slide-up-enter-active&-placement-topRight,
|
||||
&-slide-up-appear&-slide-up-appear-active&-placement-topRight {
|
||||
animation-name: rcSelectDropdownSlideDownIn;
|
||||
animation-play-state: running;
|
||||
}
|
||||
|
||||
&-slide-up-leave&-slide-up-leave-active&-placement-topLeft,
|
||||
&-slide-up-leave&-slide-up-leave-active&-placement-topRight {
|
||||
animation-name: rcSelectDropdownSlideDownOut;
|
||||
animation-play-state: running;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rcSelectDropdownSlideUpIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform-origin: 0% 0%;
|
||||
transform: scaleY(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform-origin: 0% 0%;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
@keyframes rcSelectDropdownSlideUpOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform-origin: 0% 0%;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform-origin: 0% 0%;
|
||||
transform: scaleY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rcSelectDropdownSlideDownIn {
|
||||
0% {
|
||||
transform: scaleY(0);
|
||||
transform-origin: 100% 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
transform-origin: 100% 100%;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rcSelectDropdownSlideDownOut {
|
||||
0% {
|
||||
transform: scaleY(1);
|
||||
transform-origin: 100% 100%;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scaleY(0);
|
||||
transform-origin: 100% 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rcSelectLoadingIcon {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
// This is used for semantic refactoring
|
||||
@import (reference) url('./index.less');
|
||||
|
||||
.@{select-prefix}.@{select-prefix} {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
border: 1px solid blue;
|
||||
position: relative;
|
||||
|
||||
// Content 部分自动占据剩余宽度
|
||||
.@{select-prefix}-content {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* Prevent content from wrapping */
|
||||
min-width: 0; /* allow flex item to shrink */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.@{select-prefix}-input {
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.@{select-prefix}-placeholder {
|
||||
opacity: 0.5;
|
||||
|
||||
&::after {
|
||||
content: '\00a0'; // nbsp placeholder
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.@{select-prefix}-content,
|
||||
.@{select-prefix}-input,
|
||||
.@{select-prefix}-placeholder {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
// 其他部分禁止自动宽度,使用内容宽度
|
||||
.@{select-prefix}-prefix,
|
||||
.@{select-prefix}-suffix,
|
||||
.@{select-prefix}-clear {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.@{select-prefix}-clear {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
// ============================= Single =============================
|
||||
&-single {
|
||||
.@{select-prefix}-input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ Multiple ============================
|
||||
&-multiple {
|
||||
.@{select-prefix}-selection-item {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.@{select-prefix}-placeholder {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.@{select-prefix}-input {
|
||||
width: calc(var(--select-input-width, 10) * 1px);
|
||||
min-width: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import type { DisplayValueType } from '.';
|
||||
export interface PoliteProps {
|
||||
visible: boolean;
|
||||
values: DisplayValueType[];
|
||||
}
|
||||
export default function Polite(props: PoliteProps): React.JSX.Element;
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react';
|
||||
export default function Polite(props) {
|
||||
const {
|
||||
visible,
|
||||
values
|
||||
} = props;
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only cut part of values since it's a screen reader
|
||||
const MAX_COUNT = 50;
|
||||
return /*#__PURE__*/React.createElement("span", {
|
||||
"aria-live": "polite",
|
||||
style: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
position: 'absolute',
|
||||
overflow: 'hidden',
|
||||
opacity: 0
|
||||
}
|
||||
}, `${values.slice(0, MAX_COUNT).map(({
|
||||
label,
|
||||
value
|
||||
}) => ['number', 'string'].includes(typeof label) ? label : value).join(', ')}`, values.length > MAX_COUNT ? ', ...' : null);
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
import type { AlignType, BuildInPlacements } from '@rc-component/trigger/lib/interface';
|
||||
import type { ScrollConfig, ScrollTo } from '@rc-component/virtual-list/lib/List';
|
||||
import * as React from 'react';
|
||||
import type { DisplayInfoType, DisplayValueType, Mode, Placement, RawValueType, RenderDOMFunc, RenderNode } from '../interface';
|
||||
import type { ComponentsConfig } from '../hooks/useComponents';
|
||||
export type BaseSelectSemanticName = 'prefix' | 'suffix' | 'input' | 'clear' | 'placeholder' | 'content' | 'item' | 'itemContent' | 'itemRemove';
|
||||
/**
|
||||
* ZombieJ:
|
||||
* We are currently refactoring the semantic structure of the component. Changelog:
|
||||
* - Remove `suffixIcon` and change to `suffix`.
|
||||
* - Add `components.root` for replacing response element.
|
||||
* - Remove `getInputElement` and `getRawInputElement` since we can use `components.input` instead.
|
||||
*/
|
||||
export type { DisplayInfoType, DisplayValueType, Mode, Placement, RenderDOMFunc, RenderNode, RawValueType, };
|
||||
export interface RefOptionListProps {
|
||||
onKeyDown: React.KeyboardEventHandler;
|
||||
onKeyUp: React.KeyboardEventHandler;
|
||||
scrollTo?: (args: number | ScrollConfig) => void;
|
||||
}
|
||||
export type CustomTagProps = {
|
||||
label: React.ReactNode;
|
||||
value: any;
|
||||
disabled: boolean;
|
||||
onClose: (event?: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
closable: boolean;
|
||||
isMaxTag: boolean;
|
||||
index: number;
|
||||
};
|
||||
export interface BaseSelectRef {
|
||||
focus: (options?: FocusOptions) => void;
|
||||
blur: () => void;
|
||||
scrollTo: ScrollTo;
|
||||
nativeElement: HTMLElement;
|
||||
}
|
||||
export interface BaseSelectPrivateProps {
|
||||
id: string;
|
||||
prefixCls: string;
|
||||
omitDomProps?: string[];
|
||||
displayValues: DisplayValueType[];
|
||||
onDisplayValuesChange: (values: DisplayValueType[], info: {
|
||||
type: DisplayInfoType;
|
||||
values: DisplayValueType[];
|
||||
}) => void;
|
||||
/** Current dropdown list active item string value */
|
||||
activeValue?: string;
|
||||
/** Link search input with target element */
|
||||
activeDescendantId?: string;
|
||||
onActiveValueChange?: (value: string | null) => void;
|
||||
searchValue: string;
|
||||
autoClearSearchValue?: boolean;
|
||||
/** Trigger onSearch, return false to prevent trigger open event */
|
||||
onSearch: (searchValue: string, info: {
|
||||
source: 'typing' | 'effect' | 'submit' | 'blur';
|
||||
}) => void;
|
||||
/** Trigger when search text match the `tokenSeparators`. Will provide split content */
|
||||
onSearchSplit?: (words: string[]) => void;
|
||||
OptionList: React.ForwardRefExoticComponent<React.PropsWithoutRef<any> & React.RefAttributes<RefOptionListProps>>;
|
||||
/** Tell if provided `options` is empty */
|
||||
emptyOptions: boolean;
|
||||
}
|
||||
export type BaseSelectPropsWithoutPrivate = Omit<BaseSelectProps, keyof BaseSelectPrivateProps>;
|
||||
export interface BaseSelectProps extends BaseSelectPrivateProps, React.AriaAttributes, Pick<React.HTMLAttributes<HTMLElement>, 'role'> {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
classNames?: Partial<Record<BaseSelectSemanticName, string>>;
|
||||
styles?: Partial<Record<BaseSelectSemanticName, React.CSSProperties>>;
|
||||
showSearch?: boolean;
|
||||
tagRender?: (props: CustomTagProps) => React.ReactElement;
|
||||
direction?: 'ltr' | 'rtl';
|
||||
autoFocus?: boolean;
|
||||
placeholder?: React.ReactNode;
|
||||
maxCount?: number;
|
||||
title?: string;
|
||||
tabIndex?: number;
|
||||
notFoundContent?: React.ReactNode;
|
||||
onClear?: () => void;
|
||||
maxLength?: number;
|
||||
showScrollBar?: boolean | 'optional';
|
||||
choiceTransitionName?: string;
|
||||
mode?: Mode;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
open?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
onPopupVisibleChange?: (open: boolean) => void;
|
||||
/** @private Internal usage. Do not use in your production. */
|
||||
getInputElement?: () => JSX.Element;
|
||||
/** @private Internal usage. Do not use in your production. */
|
||||
getRawInputElement?: () => JSX.Element;
|
||||
maxTagTextLength?: number;
|
||||
maxTagCount?: number | 'responsive';
|
||||
maxTagPlaceholder?: React.ReactNode | ((omittedValues: DisplayValueType[]) => React.ReactNode);
|
||||
tokenSeparators?: string[];
|
||||
allowClear?: boolean | {
|
||||
clearIcon?: React.ReactNode;
|
||||
};
|
||||
prefix?: React.ReactNode;
|
||||
/** @deprecated Please use `suffix` instead. */
|
||||
suffixIcon?: RenderNode;
|
||||
suffix?: RenderNode;
|
||||
/**
|
||||
* Clear all icon
|
||||
* @deprecated Please use `allowClear` instead
|
||||
**/
|
||||
clearIcon?: React.ReactNode;
|
||||
/** Selector remove icon */
|
||||
removeIcon?: RenderNode;
|
||||
animation?: string;
|
||||
transitionName?: string;
|
||||
popupStyle?: React.CSSProperties;
|
||||
popupClassName?: string;
|
||||
popupMatchSelectWidth?: boolean | number;
|
||||
popupRender?: (menu: React.ReactElement) => React.ReactElement;
|
||||
popupAlign?: AlignType;
|
||||
placement?: Placement;
|
||||
builtinPlacements?: BuildInPlacements;
|
||||
getPopupContainer?: RenderDOMFunc;
|
||||
showAction?: ('focus' | 'click')[];
|
||||
onBlur?: React.FocusEventHandler<HTMLElement>;
|
||||
onFocus?: React.FocusEventHandler<HTMLElement>;
|
||||
onKeyUp?: React.KeyboardEventHandler<HTMLDivElement>;
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
|
||||
onMouseDown?: React.MouseEventHandler<HTMLDivElement>;
|
||||
onPopupScroll?: React.UIEventHandler<HTMLDivElement>;
|
||||
onInputKeyDown?: React.KeyboardEventHandler<HTMLInputElement | HTMLTextAreaElement>;
|
||||
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
|
||||
onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
components?: ComponentsConfig;
|
||||
}
|
||||
export declare const isMultiple: (mode: Mode) => boolean;
|
||||
declare const BaseSelect: React.ForwardRefExoticComponent<BaseSelectProps & React.RefAttributes<BaseSelectRef>>;
|
||||
export default BaseSelect;
|
||||
+528
@@ -0,0 +1,528 @@
|
||||
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
||||
import { clsx } from 'clsx';
|
||||
import { getDOM } from "@rc-component/util/es/Dom/findDOMNode";
|
||||
import * as React from 'react';
|
||||
import { useAllowClear } from "../hooks/useAllowClear";
|
||||
import { BaseSelectContext } from "../hooks/useBaseProps";
|
||||
import useLock from "../hooks/useLock";
|
||||
import useSelectTriggerControl, { isInside } from "../hooks/useSelectTriggerControl";
|
||||
import SelectTrigger from "../SelectTrigger";
|
||||
import { getSeparatedContent, isValidCount } from "../utils/valueUtil";
|
||||
import Polite from "./Polite";
|
||||
import useOpen, { macroTask } from "../hooks/useOpen";
|
||||
import { useEvent } from '@rc-component/util';
|
||||
import SelectInput from "../SelectInput";
|
||||
import useComponents from "../hooks/useComponents";
|
||||
|
||||
/**
|
||||
* ZombieJ:
|
||||
* We are currently refactoring the semantic structure of the component. Changelog:
|
||||
* - Remove `suffixIcon` and change to `suffix`.
|
||||
* - Add `components.root` for replacing response element.
|
||||
* - Remove `getInputElement` and `getRawInputElement` since we can use `components.input` instead.
|
||||
*/
|
||||
|
||||
export const isMultiple = mode => mode === 'tags' || mode === 'multiple';
|
||||
const BaseSelect = /*#__PURE__*/React.forwardRef((props, ref) => {
|
||||
const {
|
||||
id,
|
||||
prefixCls,
|
||||
className,
|
||||
styles,
|
||||
classNames,
|
||||
showSearch,
|
||||
tagRender,
|
||||
showScrollBar = 'optional',
|
||||
direction,
|
||||
omitDomProps,
|
||||
// Value
|
||||
displayValues,
|
||||
onDisplayValuesChange,
|
||||
emptyOptions,
|
||||
notFoundContent = 'Not Found',
|
||||
onClear,
|
||||
maxCount,
|
||||
placeholder,
|
||||
// Mode
|
||||
mode,
|
||||
// Status
|
||||
disabled,
|
||||
loading,
|
||||
// Customize Input
|
||||
getInputElement,
|
||||
getRawInputElement,
|
||||
// Open
|
||||
open,
|
||||
defaultOpen,
|
||||
onPopupVisibleChange,
|
||||
// Active
|
||||
activeValue,
|
||||
onActiveValueChange,
|
||||
activeDescendantId,
|
||||
// Search
|
||||
searchValue,
|
||||
autoClearSearchValue,
|
||||
onSearch,
|
||||
onSearchSplit,
|
||||
tokenSeparators,
|
||||
// Icons
|
||||
allowClear,
|
||||
prefix,
|
||||
suffix,
|
||||
suffixIcon,
|
||||
clearIcon,
|
||||
// Dropdown
|
||||
OptionList,
|
||||
animation,
|
||||
transitionName,
|
||||
popupStyle,
|
||||
popupClassName,
|
||||
popupMatchSelectWidth,
|
||||
popupRender,
|
||||
popupAlign,
|
||||
placement,
|
||||
builtinPlacements,
|
||||
getPopupContainer,
|
||||
// Focus
|
||||
showAction = [],
|
||||
onFocus,
|
||||
onBlur,
|
||||
// Rest Events
|
||||
onKeyUp,
|
||||
onKeyDown,
|
||||
onMouseDown,
|
||||
// Components
|
||||
components,
|
||||
// Rest Props
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
// ============================== MISC ==============================
|
||||
const multiple = isMultiple(mode);
|
||||
|
||||
// ============================== Refs ==============================
|
||||
const containerRef = React.useRef(null);
|
||||
const triggerRef = React.useRef(null);
|
||||
const listRef = React.useRef(null);
|
||||
|
||||
/** Used for component focused management */
|
||||
const [focused, setFocused] = React.useState(false);
|
||||
|
||||
// =========================== Imperative ===========================
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
focus: containerRef.current?.focus,
|
||||
blur: containerRef.current?.blur,
|
||||
scrollTo: arg => listRef.current?.scrollTo(arg),
|
||||
nativeElement: getDOM(containerRef.current)
|
||||
}));
|
||||
|
||||
// =========================== Components ===========================
|
||||
const mergedComponents = useComponents(components, getInputElement, getRawInputElement);
|
||||
|
||||
// ========================== Search Value ==========================
|
||||
const mergedSearchValue = React.useMemo(() => {
|
||||
if (mode !== 'combobox') {
|
||||
return searchValue;
|
||||
}
|
||||
const val = displayValues[0]?.value;
|
||||
return typeof val === 'string' || typeof val === 'number' ? String(val) : '';
|
||||
}, [searchValue, mode, displayValues]);
|
||||
|
||||
// ========================== Custom Input ==========================
|
||||
// Only works in `combobox`
|
||||
const customizeInputElement = mode === 'combobox' && typeof getInputElement === 'function' && getInputElement() || null;
|
||||
|
||||
// ============================== Open ==============================
|
||||
// Not trigger `open` when `notFoundContent` is empty
|
||||
const emptyListContent = !notFoundContent && emptyOptions;
|
||||
const [rawOpen, mergedOpen, triggerOpen, lockOptions] = useOpen(defaultOpen || false, open, onPopupVisibleChange, nextOpen => disabled || emptyListContent ? false : nextOpen);
|
||||
|
||||
// ============================= Search =============================
|
||||
const tokenWithEnter = React.useMemo(() => (tokenSeparators || []).some(tokenSeparator => ['\n', '\r\n'].includes(tokenSeparator)), [tokenSeparators]);
|
||||
const onInternalSearch = (searchText, fromTyping, isCompositing) => {
|
||||
if (multiple && isValidCount(maxCount) && displayValues.length >= maxCount) {
|
||||
return;
|
||||
}
|
||||
let ret = true;
|
||||
let newSearchText = searchText;
|
||||
onActiveValueChange?.(null);
|
||||
const separatedList = getSeparatedContent(searchText, tokenSeparators, isValidCount(maxCount) ? maxCount - displayValues.length : undefined);
|
||||
|
||||
// Check if match the `tokenSeparators`
|
||||
const patchLabels = isCompositing ? null : separatedList;
|
||||
|
||||
// Ignore combobox since it's not split-able
|
||||
if (mode !== 'combobox' && patchLabels) {
|
||||
newSearchText = '';
|
||||
onSearchSplit?.(patchLabels);
|
||||
|
||||
// Should close when paste finish
|
||||
triggerOpen(false);
|
||||
|
||||
// Tell Selector that break next actions
|
||||
ret = false;
|
||||
}
|
||||
if (onSearch && mergedSearchValue !== newSearchText) {
|
||||
onSearch(newSearchText, {
|
||||
source: fromTyping ? 'typing' : 'effect'
|
||||
});
|
||||
}
|
||||
|
||||
// Open if from typing
|
||||
if (searchText && fromTyping && ret) {
|
||||
triggerOpen(true);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Only triggered when menu is closed & mode is tags
|
||||
// If menu is open, OptionList will take charge
|
||||
// If mode isn't tags, press enter is not meaningful when you can't see any option
|
||||
const onInternalSearchSubmit = searchText => {
|
||||
// prevent empty tags from appearing when you click the Enter button
|
||||
if (!searchText || !searchText.trim()) {
|
||||
return;
|
||||
}
|
||||
onSearch(searchText, {
|
||||
source: 'submit'
|
||||
});
|
||||
};
|
||||
|
||||
// Clean up search value when the dropdown is closed.
|
||||
// We use `rawOpen` here to avoid clearing the search input when the dropdown is
|
||||
// programmatically closed due to `notFoundContent={null}` and no matching options.
|
||||
// This allows the user to continue typing their search query.
|
||||
React.useEffect(() => {
|
||||
if (!rawOpen && !multiple && mode !== 'combobox') {
|
||||
onInternalSearch('', false, false);
|
||||
}
|
||||
}, [rawOpen]);
|
||||
|
||||
// ============================ Disabled ============================
|
||||
// Close dropdown & remove focus state when disabled change
|
||||
React.useEffect(() => {
|
||||
// After onBlur is triggered, the focused does not need to be reset
|
||||
if (disabled) {
|
||||
triggerOpen(false);
|
||||
setFocused(false);
|
||||
}
|
||||
}, [disabled, mergedOpen]);
|
||||
|
||||
// ============================ Keyboard ============================
|
||||
/**
|
||||
* We record input value here to check if can press to clean up by backspace
|
||||
* - null: Key is not down, this is reset by key up
|
||||
* - true: Search text is empty when first time backspace down
|
||||
* - false: Search text is not empty when first time backspace down
|
||||
*/
|
||||
const [getClearLock, setClearLock] = useLock();
|
||||
const keyLockRef = React.useRef(false);
|
||||
|
||||
// KeyDown
|
||||
const onInternalKeyDown = event => {
|
||||
const clearLock = getClearLock();
|
||||
const {
|
||||
key
|
||||
} = event;
|
||||
const isEnterKey = key === 'Enter';
|
||||
const isSpaceKey = key === ' ';
|
||||
|
||||
// Enter or Space opens dropdown (ARIA combobox: spacebar should open)
|
||||
if (isEnterKey || isSpaceKey) {
|
||||
// Do not submit form when type in the input; prevent Space from scrolling page
|
||||
const isCombobox = mode === 'combobox';
|
||||
const isEditable = isCombobox || showSearch;
|
||||
if (isSpaceKey && !isEditable || isEnterKey && !isCombobox) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// We only manage open state here, close logic should handle by list component
|
||||
if (!mergedOpen) {
|
||||
triggerOpen(true);
|
||||
}
|
||||
}
|
||||
setClearLock(!!mergedSearchValue);
|
||||
|
||||
// Remove value by `backspace`
|
||||
if (key === 'Backspace' && !clearLock && multiple && !mergedSearchValue && displayValues.length) {
|
||||
const cloneDisplayValues = [...displayValues];
|
||||
let removedDisplayValue = null;
|
||||
for (let i = cloneDisplayValues.length - 1; i >= 0; i -= 1) {
|
||||
const current = cloneDisplayValues[i];
|
||||
if (!current.disabled) {
|
||||
cloneDisplayValues.splice(i, 1);
|
||||
removedDisplayValue = current;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (removedDisplayValue) {
|
||||
onDisplayValuesChange(cloneDisplayValues, {
|
||||
type: 'remove',
|
||||
values: [removedDisplayValue]
|
||||
});
|
||||
}
|
||||
}
|
||||
if (mergedOpen && (!isEnterKey || !keyLockRef.current) && !isSpaceKey) {
|
||||
// Lock the Enter key after it is pressed to avoid repeated triggering of the onChange event.
|
||||
if (isEnterKey) {
|
||||
keyLockRef.current = true;
|
||||
}
|
||||
listRef.current?.onKeyDown(event);
|
||||
}
|
||||
onKeyDown?.(event);
|
||||
};
|
||||
|
||||
// KeyUp
|
||||
const onInternalKeyUp = (event, ...rest) => {
|
||||
if (mergedOpen) {
|
||||
listRef.current?.onKeyUp(event, ...rest);
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
keyLockRef.current = false;
|
||||
}
|
||||
onKeyUp?.(event, ...rest);
|
||||
};
|
||||
|
||||
// ============================ Selector ============================
|
||||
const onSelectorRemove = useEvent(val => {
|
||||
const newValues = displayValues.filter(i => i !== val);
|
||||
onDisplayValuesChange(newValues, {
|
||||
type: 'remove',
|
||||
values: [val]
|
||||
});
|
||||
});
|
||||
const onInputBlur = () => {
|
||||
// Unlock the Enter key after the input blur; otherwise, the Enter key needs to be pressed twice to trigger the correct effect.
|
||||
keyLockRef.current = false;
|
||||
};
|
||||
|
||||
// ========================== Focus / Blur ==========================
|
||||
const getSelectElements = () => [getDOM(containerRef.current), triggerRef.current?.getPopupElement()];
|
||||
|
||||
// Close when click on non-select element
|
||||
useSelectTriggerControl(getSelectElements, mergedOpen, triggerOpen, !!mergedComponents.root);
|
||||
|
||||
// ========================== Focus / Blur ==========================
|
||||
const internalMouseDownRef = React.useRef(false);
|
||||
const onInternalFocus = event => {
|
||||
setFocused(true);
|
||||
if (!disabled) {
|
||||
// `showAction` should handle `focus` if set
|
||||
if (showAction.includes('focus')) {
|
||||
triggerOpen(true);
|
||||
}
|
||||
onFocus?.(event);
|
||||
}
|
||||
};
|
||||
const onRootBlur = () => {
|
||||
// Delay close should check the activeElement
|
||||
if (mergedOpen && !internalMouseDownRef.current) {
|
||||
triggerOpen(false, {
|
||||
cancelFun: () => isInside(getSelectElements(), document.activeElement)
|
||||
});
|
||||
}
|
||||
};
|
||||
const onInternalBlur = event => {
|
||||
setFocused(false);
|
||||
if (mergedSearchValue) {
|
||||
// `tags` mode should move `searchValue` into values
|
||||
if (mode === 'tags') {
|
||||
onSearch(mergedSearchValue, {
|
||||
source: 'submit'
|
||||
});
|
||||
} else if (mode === 'multiple') {
|
||||
// `multiple` mode only clean the search value but not trigger event
|
||||
onSearch('', {
|
||||
source: 'blur'
|
||||
});
|
||||
}
|
||||
}
|
||||
onRootBlur();
|
||||
if (!disabled) {
|
||||
onBlur?.(event);
|
||||
}
|
||||
};
|
||||
const onRootMouseDown = (event, ...restArgs) => {
|
||||
const {
|
||||
target
|
||||
} = event;
|
||||
const popupElement = triggerRef.current?.getPopupElement();
|
||||
|
||||
// We should give focus back to selector if clicked item is not focusable
|
||||
if (popupElement?.contains(target) && triggerOpen) {
|
||||
// Tell `open` not to close since it's safe in the popup
|
||||
triggerOpen(true);
|
||||
}
|
||||
onMouseDown?.(event, ...restArgs);
|
||||
internalMouseDownRef.current = true;
|
||||
macroTask(() => {
|
||||
internalMouseDownRef.current = false;
|
||||
});
|
||||
};
|
||||
|
||||
// ============================ Dropdown ============================
|
||||
const [, forceUpdate] = React.useState({});
|
||||
// We need force update here since popup dom is render async
|
||||
function onPopupMouseEnter() {
|
||||
forceUpdate({});
|
||||
}
|
||||
|
||||
// Used for raw custom input trigger
|
||||
let onTriggerVisibleChange;
|
||||
if (!!mergedComponents.root) {
|
||||
onTriggerVisibleChange = newOpen => {
|
||||
triggerOpen(newOpen);
|
||||
};
|
||||
}
|
||||
|
||||
// ============================ Context =============================
|
||||
const baseSelectContext = React.useMemo(() => ({
|
||||
...props,
|
||||
notFoundContent,
|
||||
open: mergedOpen,
|
||||
triggerOpen: mergedOpen,
|
||||
rawOpen,
|
||||
id,
|
||||
showSearch,
|
||||
multiple,
|
||||
toggleOpen: triggerOpen,
|
||||
showScrollBar,
|
||||
styles,
|
||||
classNames,
|
||||
lockOptions
|
||||
}), [props, notFoundContent, triggerOpen, id, showSearch, multiple, mergedOpen, rawOpen, showScrollBar, styles, classNames, lockOptions]);
|
||||
|
||||
// ==================================================================
|
||||
// == Render ==
|
||||
// ==================================================================
|
||||
|
||||
// ============================= Suffix =============================
|
||||
const mergedSuffixIcon = React.useMemo(() => {
|
||||
const nextSuffix = suffix ?? suffixIcon;
|
||||
if (typeof nextSuffix === 'function') {
|
||||
return nextSuffix({
|
||||
searchValue: mergedSearchValue,
|
||||
open: mergedOpen,
|
||||
focused,
|
||||
showSearch,
|
||||
loading
|
||||
});
|
||||
}
|
||||
return nextSuffix;
|
||||
}, [suffix, suffixIcon, mergedSearchValue, mergedOpen, focused, showSearch, loading]);
|
||||
|
||||
// ============================= Clear ==============================
|
||||
const onClearMouseDown = () => {
|
||||
onClear?.();
|
||||
containerRef.current?.focus();
|
||||
onDisplayValuesChange([], {
|
||||
type: 'clear',
|
||||
values: displayValues
|
||||
});
|
||||
onInternalSearch('', false, false);
|
||||
};
|
||||
const {
|
||||
allowClear: mergedAllowClear,
|
||||
clearIcon: clearNode
|
||||
} = useAllowClear(prefixCls, displayValues, allowClear, clearIcon, disabled, mergedSearchValue, mode);
|
||||
|
||||
// =========================== OptionList ===========================
|
||||
const optionList = /*#__PURE__*/React.createElement(OptionList, {
|
||||
ref: listRef
|
||||
});
|
||||
|
||||
// ============================= Select =============================
|
||||
const mergedClassName = clsx(prefixCls, className, {
|
||||
[`${prefixCls}-focused`]: focused,
|
||||
[`${prefixCls}-multiple`]: multiple,
|
||||
[`${prefixCls}-single`]: !multiple,
|
||||
[`${prefixCls}-allow-clear`]: mergedAllowClear,
|
||||
[`${prefixCls}-show-arrow`]: mergedSuffixIcon !== undefined && mergedSuffixIcon !== null,
|
||||
[`${prefixCls}-disabled`]: disabled,
|
||||
[`${prefixCls}-loading`]: loading,
|
||||
[`${prefixCls}-open`]: mergedOpen,
|
||||
[`${prefixCls}-customize-input`]: customizeInputElement,
|
||||
[`${prefixCls}-show-search`]: showSearch
|
||||
});
|
||||
|
||||
// >>> Render
|
||||
let renderNode = /*#__PURE__*/React.createElement(SelectInput, _extends({}, restProps, {
|
||||
// Ref
|
||||
ref: containerRef
|
||||
// Style
|
||||
,
|
||||
prefixCls: prefixCls,
|
||||
className: mergedClassName
|
||||
// Focus state
|
||||
,
|
||||
focused: focused
|
||||
// UI
|
||||
,
|
||||
prefix: prefix,
|
||||
suffix: mergedSuffixIcon,
|
||||
clearIcon: clearNode
|
||||
// Type or mode
|
||||
,
|
||||
multiple: multiple,
|
||||
mode: mode
|
||||
// Values
|
||||
,
|
||||
displayValues: displayValues,
|
||||
placeholder: placeholder,
|
||||
searchValue: mergedSearchValue,
|
||||
activeValue: activeValue,
|
||||
onSearch: onInternalSearch,
|
||||
onSearchSubmit: onInternalSearchSubmit,
|
||||
onInputBlur: onInputBlur,
|
||||
onFocus: onInternalFocus,
|
||||
onBlur: onInternalBlur,
|
||||
onClearMouseDown: onClearMouseDown,
|
||||
onKeyDown: onInternalKeyDown,
|
||||
onKeyUp: onInternalKeyUp,
|
||||
onSelectorRemove: onSelectorRemove
|
||||
// Token handling
|
||||
,
|
||||
tokenWithEnter: tokenWithEnter
|
||||
// Open
|
||||
,
|
||||
onMouseDown: onRootMouseDown
|
||||
// Components
|
||||
,
|
||||
components: mergedComponents
|
||||
}));
|
||||
renderNode = /*#__PURE__*/React.createElement(SelectTrigger, {
|
||||
ref: triggerRef,
|
||||
disabled: disabled,
|
||||
prefixCls: prefixCls,
|
||||
visible: mergedOpen,
|
||||
popupElement: optionList,
|
||||
animation: animation,
|
||||
transitionName: transitionName,
|
||||
popupStyle: popupStyle,
|
||||
popupClassName: popupClassName,
|
||||
direction: direction,
|
||||
popupMatchSelectWidth: popupMatchSelectWidth,
|
||||
popupRender: popupRender,
|
||||
popupAlign: popupAlign,
|
||||
placement: placement,
|
||||
builtinPlacements: builtinPlacements,
|
||||
getPopupContainer: getPopupContainer,
|
||||
empty: emptyOptions,
|
||||
onPopupVisibleChange: onTriggerVisibleChange,
|
||||
onPopupMouseEnter: onPopupMouseEnter,
|
||||
onPopupMouseDown: onRootMouseDown,
|
||||
onPopupBlur: onRootBlur
|
||||
}, renderNode);
|
||||
return /*#__PURE__*/React.createElement(BaseSelectContext.Provider, {
|
||||
value: baseSelectContext
|
||||
}, /*#__PURE__*/React.createElement(Polite, {
|
||||
visible: focused && !mergedOpen,
|
||||
values: displayValues
|
||||
}), renderNode);
|
||||
});
|
||||
|
||||
// Set display name for dev
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
BaseSelect.displayName = 'BaseSelect';
|
||||
}
|
||||
export default BaseSelect;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import type * as React from 'react';
|
||||
import type { DefaultOptionType } from './Select';
|
||||
export interface OptGroupProps extends Omit<DefaultOptionType, 'options'> {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
export interface OptionGroupFC extends React.FC<OptGroupProps> {
|
||||
/** Legacy for check if is a Option Group */
|
||||
isSelectOptGroup: boolean;
|
||||
}
|
||||
/** This is a placeholder, not real render in dom */
|
||||
declare const OptGroup: OptionGroupFC;
|
||||
export default OptGroup;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
/** This is a placeholder, not real render in dom */
|
||||
const OptGroup = () => null;
|
||||
OptGroup.isSelectOptGroup = true;
|
||||
export default OptGroup;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import type * as React from 'react';
|
||||
import type { DefaultOptionType } from './Select';
|
||||
export interface OptionProps extends Omit<DefaultOptionType, 'label'> {
|
||||
children: React.ReactNode;
|
||||
/** Save for customize data */
|
||||
[prop: string]: any;
|
||||
}
|
||||
export interface OptionFC extends React.FC<OptionProps> {
|
||||
/** Legacy for check if is a Option Group */
|
||||
isSelectOption: boolean;
|
||||
}
|
||||
/** This is a placeholder, not real render in dom */
|
||||
declare const Option: OptionFC;
|
||||
export default Option;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
/* istanbul ignore file */
|
||||
|
||||
/** This is a placeholder, not real render in dom */
|
||||
const Option = () => null;
|
||||
Option.isSelectOption = true;
|
||||
export default Option;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import type { ScrollConfig } from '@rc-component/virtual-list/lib/List';
|
||||
import * as React from 'react';
|
||||
export type OptionListProps = Record<string, never>;
|
||||
export interface RefOptionListProps {
|
||||
onKeyDown: React.KeyboardEventHandler;
|
||||
onKeyUp: React.KeyboardEventHandler;
|
||||
scrollTo?: (args: number | ScrollConfig) => void;
|
||||
}
|
||||
declare const RefOptionList: React.ForwardRefExoticComponent<React.RefAttributes<RefOptionListProps>>;
|
||||
export default RefOptionList;
|
||||
+395
@@ -0,0 +1,395 @@
|
||||
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
||||
import { clsx } from 'clsx';
|
||||
import KeyCode from "@rc-component/util/es/KeyCode";
|
||||
import useMemo from "@rc-component/util/es/hooks/useMemo";
|
||||
import omit from "@rc-component/util/es/omit";
|
||||
import pickAttrs from "@rc-component/util/es/pickAttrs";
|
||||
import List from '@rc-component/virtual-list';
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import SelectContext from "./SelectContext";
|
||||
import TransBtn from "./TransBtn";
|
||||
import useBaseProps from "./hooks/useBaseProps";
|
||||
import { isPlatformMac } from "./utils/platformUtil";
|
||||
import { isValidCount } from "./utils/valueUtil";
|
||||
|
||||
// export interface OptionListProps<OptionsType extends object[]> {
|
||||
|
||||
function isTitleType(content) {
|
||||
return typeof content === 'string' || typeof content === 'number';
|
||||
}
|
||||
|
||||
/**
|
||||
* Using virtual list of option display.
|
||||
* Will fallback to dom if use customize render.
|
||||
*/
|
||||
const OptionList = (_, ref) => {
|
||||
const {
|
||||
prefixCls,
|
||||
id,
|
||||
open,
|
||||
multiple,
|
||||
mode,
|
||||
searchValue,
|
||||
toggleOpen,
|
||||
notFoundContent,
|
||||
onPopupScroll,
|
||||
showScrollBar,
|
||||
lockOptions
|
||||
} = useBaseProps();
|
||||
const {
|
||||
maxCount,
|
||||
flattenOptions,
|
||||
onActiveValue,
|
||||
defaultActiveFirstOption,
|
||||
onSelect,
|
||||
menuItemSelectedIcon,
|
||||
rawValues,
|
||||
fieldNames,
|
||||
virtual,
|
||||
direction,
|
||||
listHeight,
|
||||
listItemHeight,
|
||||
optionRender,
|
||||
classNames: contextClassNames,
|
||||
styles: contextStyles
|
||||
} = React.useContext(SelectContext);
|
||||
const itemPrefixCls = `${prefixCls}-item`;
|
||||
const memoFlattenOptions = useMemo(() => flattenOptions, [open, lockOptions], (prev, next) => next[0] && !next[1]);
|
||||
|
||||
// =========================== List ===========================
|
||||
const listRef = React.useRef(null);
|
||||
const overMaxCount = React.useMemo(() => multiple && isValidCount(maxCount) && rawValues?.size >= maxCount, [multiple, maxCount, rawValues?.size]);
|
||||
const onListMouseDown = event => {
|
||||
event.preventDefault();
|
||||
};
|
||||
const scrollIntoView = args => {
|
||||
listRef.current?.scrollTo(typeof args === 'number' ? {
|
||||
index: args
|
||||
} : args);
|
||||
};
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/34975
|
||||
const isSelected = React.useCallback(value => {
|
||||
if (mode === 'combobox') {
|
||||
return false;
|
||||
}
|
||||
return rawValues.has(value);
|
||||
}, [mode, [...rawValues].toString(), rawValues.size]);
|
||||
|
||||
// ========================== Active ==========================
|
||||
const getEnabledActiveIndex = (index, offset = 1) => {
|
||||
const len = memoFlattenOptions.length;
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
const current = (index + i * offset + len) % len;
|
||||
const {
|
||||
group,
|
||||
data
|
||||
} = memoFlattenOptions[current] || {};
|
||||
if (!group && !data?.disabled && (isSelected(data.value) || !overMaxCount)) {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
const [activeIndex, setActiveIndex] = React.useState(() => getEnabledActiveIndex(0));
|
||||
const setActive = (index, fromKeyboard = false) => {
|
||||
setActiveIndex(index);
|
||||
const info = {
|
||||
source: fromKeyboard ? 'keyboard' : 'mouse'
|
||||
};
|
||||
|
||||
// Trigger active event
|
||||
const flattenItem = memoFlattenOptions[index];
|
||||
if (!flattenItem) {
|
||||
onActiveValue(null, -1, info);
|
||||
return;
|
||||
}
|
||||
onActiveValue(flattenItem.value, index, info);
|
||||
};
|
||||
|
||||
// Auto active first item when list length or searchValue changed
|
||||
useEffect(() => {
|
||||
setActive(defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1);
|
||||
}, [memoFlattenOptions.length, searchValue]);
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/48036
|
||||
const isAriaSelected = React.useCallback(value => {
|
||||
if (mode === 'combobox') {
|
||||
return String(value).toLowerCase() === searchValue.toLowerCase();
|
||||
}
|
||||
return rawValues.has(value);
|
||||
}, [mode, searchValue, [...rawValues].toString(), rawValues.size]);
|
||||
|
||||
// Auto scroll to item position in single mode
|
||||
useEffect(() => {
|
||||
/**
|
||||
* React will skip `onChange` when component update.
|
||||
* `setActive` function will call root accessibility state update which makes re-render.
|
||||
* So we need to delay to let Input component trigger onChange first.
|
||||
*/
|
||||
let timeoutId;
|
||||
if (!multiple && open && rawValues.size === 1) {
|
||||
const value = Array.from(rawValues)[0];
|
||||
// Scroll to the option closest to the searchValue if searching.
|
||||
const index = memoFlattenOptions.findIndex(({
|
||||
data
|
||||
}) => searchValue ? String(data.value).startsWith(searchValue) : data.value === value);
|
||||
if (index !== -1) {
|
||||
setActive(index);
|
||||
timeoutId = setTimeout(() => {
|
||||
scrollIntoView(index);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Force trigger scrollbar visible when open
|
||||
if (open) {
|
||||
listRef.current?.scrollTo(undefined);
|
||||
}
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [open, searchValue]);
|
||||
|
||||
// ========================== Values ==========================
|
||||
const onSelectValue = value => {
|
||||
if (value !== undefined) {
|
||||
onSelect(value, {
|
||||
selected: !rawValues.has(value)
|
||||
});
|
||||
}
|
||||
|
||||
// Single mode should always close by select
|
||||
if (!multiple) {
|
||||
toggleOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ========================= Keyboard =========================
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
onKeyDown: event => {
|
||||
const {
|
||||
which,
|
||||
ctrlKey
|
||||
} = event;
|
||||
switch (which) {
|
||||
// >>> Arrow keys & ctrl + n/p on Mac
|
||||
case KeyCode.N:
|
||||
case KeyCode.P:
|
||||
case KeyCode.UP:
|
||||
case KeyCode.DOWN:
|
||||
{
|
||||
let offset = 0;
|
||||
if (which === KeyCode.UP) {
|
||||
offset = -1;
|
||||
} else if (which === KeyCode.DOWN) {
|
||||
offset = 1;
|
||||
} else if (isPlatformMac() && ctrlKey) {
|
||||
if (which === KeyCode.N) {
|
||||
offset = 1;
|
||||
} else if (which === KeyCode.P) {
|
||||
offset = -1;
|
||||
}
|
||||
}
|
||||
if (offset !== 0) {
|
||||
const nextActiveIndex = getEnabledActiveIndex(activeIndex + offset, offset);
|
||||
scrollIntoView(nextActiveIndex);
|
||||
setActive(nextActiveIndex, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// >>> Select (Tab / Enter)
|
||||
case KeyCode.TAB:
|
||||
case KeyCode.ENTER:
|
||||
{
|
||||
// value
|
||||
const item = memoFlattenOptions[activeIndex];
|
||||
if (!item || item.data.disabled) {
|
||||
return onSelectValue(undefined);
|
||||
}
|
||||
if (!overMaxCount || rawValues.has(item.value)) {
|
||||
onSelectValue(item.value);
|
||||
} else {
|
||||
onSelectValue(undefined);
|
||||
}
|
||||
if (open) {
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// >>> Close
|
||||
case KeyCode.ESC:
|
||||
{
|
||||
toggleOpen(false);
|
||||
if (open) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onKeyUp: () => {},
|
||||
scrollTo: index => {
|
||||
scrollIntoView(index);
|
||||
}
|
||||
}));
|
||||
|
||||
// ========================== Render ==========================
|
||||
if (memoFlattenOptions.length === 0) {
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
role: "listbox",
|
||||
id: `${id}_list`,
|
||||
className: `${itemPrefixCls}-empty`,
|
||||
onMouseDown: onListMouseDown
|
||||
}, notFoundContent);
|
||||
}
|
||||
const omitFieldNameList = Object.keys(fieldNames).map(key => fieldNames[key]);
|
||||
const getLabel = item => item.label;
|
||||
function getItemAriaProps(item, index) {
|
||||
const {
|
||||
group
|
||||
} = item;
|
||||
return {
|
||||
role: group ? 'presentation' : 'option',
|
||||
id: `${id}_list_${index}`
|
||||
};
|
||||
}
|
||||
const renderItem = index => {
|
||||
const item = memoFlattenOptions[index];
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
const itemData = item.data || {};
|
||||
const {
|
||||
value,
|
||||
disabled
|
||||
} = itemData;
|
||||
const {
|
||||
group
|
||||
} = item;
|
||||
const attrs = pickAttrs(itemData, true);
|
||||
const mergedLabel = getLabel(item);
|
||||
return item ? /*#__PURE__*/React.createElement("div", _extends({
|
||||
"aria-label": typeof mergedLabel === 'string' && !group ? mergedLabel : null
|
||||
}, attrs, {
|
||||
key: index
|
||||
}, getItemAriaProps(item, index), {
|
||||
"aria-selected": isAriaSelected(value),
|
||||
"aria-disabled": disabled
|
||||
}), value) : null;
|
||||
};
|
||||
const a11yProps = {
|
||||
role: 'listbox',
|
||||
id: `${id}_list`
|
||||
};
|
||||
return /*#__PURE__*/React.createElement(React.Fragment, null, virtual && /*#__PURE__*/React.createElement("div", _extends({}, a11yProps, {
|
||||
style: {
|
||||
height: 0,
|
||||
width: 0,
|
||||
overflow: 'hidden'
|
||||
}
|
||||
}), renderItem(activeIndex - 1), renderItem(activeIndex), renderItem(activeIndex + 1)), /*#__PURE__*/React.createElement(List, {
|
||||
itemKey: "key",
|
||||
ref: listRef,
|
||||
data: memoFlattenOptions,
|
||||
height: listHeight,
|
||||
itemHeight: listItemHeight,
|
||||
fullHeight: false,
|
||||
onMouseDown: onListMouseDown,
|
||||
onScroll: onPopupScroll,
|
||||
virtual: virtual,
|
||||
direction: direction,
|
||||
innerProps: virtual ? null : a11yProps,
|
||||
showScrollBar: showScrollBar,
|
||||
className: contextClassNames?.popup?.list,
|
||||
style: contextStyles?.popup?.list
|
||||
}, (item, itemIndex) => {
|
||||
const {
|
||||
group,
|
||||
groupOption,
|
||||
data,
|
||||
label,
|
||||
value
|
||||
} = item;
|
||||
const {
|
||||
key
|
||||
} = data;
|
||||
|
||||
// Group
|
||||
if (group) {
|
||||
const groupTitle = data.title ?? (isTitleType(label) ? label.toString() : undefined);
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
className: clsx(itemPrefixCls, `${itemPrefixCls}-group`, data.className),
|
||||
title: groupTitle
|
||||
}, label !== undefined ? label : key);
|
||||
}
|
||||
const {
|
||||
disabled,
|
||||
title,
|
||||
children,
|
||||
style,
|
||||
className,
|
||||
...otherProps
|
||||
} = data;
|
||||
const passedProps = omit(otherProps, omitFieldNameList);
|
||||
|
||||
// Option
|
||||
const selected = isSelected(value);
|
||||
const mergedDisabled = disabled || !selected && overMaxCount;
|
||||
const optionPrefixCls = `${itemPrefixCls}-option`;
|
||||
const optionClassName = clsx(itemPrefixCls, optionPrefixCls, className, contextClassNames?.popup?.listItem, {
|
||||
[`${optionPrefixCls}-grouped`]: groupOption,
|
||||
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !mergedDisabled,
|
||||
[`${optionPrefixCls}-disabled`]: mergedDisabled,
|
||||
[`${optionPrefixCls}-selected`]: selected
|
||||
});
|
||||
const mergedLabel = getLabel(item);
|
||||
const iconVisible = !menuItemSelectedIcon || typeof menuItemSelectedIcon === 'function' || selected;
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/34145
|
||||
const content = typeof mergedLabel === 'number' ? mergedLabel : mergedLabel || value;
|
||||
// https://github.com/ant-design/ant-design/issues/26717
|
||||
let optionTitle = isTitleType(content) ? content.toString() : undefined;
|
||||
if (title !== undefined) {
|
||||
optionTitle = title;
|
||||
}
|
||||
return /*#__PURE__*/React.createElement("div", _extends({}, pickAttrs(passedProps), !virtual ? getItemAriaProps(item, itemIndex) : {}, {
|
||||
"aria-selected": virtual ? undefined : isAriaSelected(value),
|
||||
"aria-disabled": mergedDisabled,
|
||||
className: optionClassName,
|
||||
title: optionTitle,
|
||||
onMouseMove: () => {
|
||||
if (activeIndex === itemIndex || mergedDisabled) {
|
||||
return;
|
||||
}
|
||||
setActive(itemIndex);
|
||||
},
|
||||
onClick: () => {
|
||||
if (!mergedDisabled) {
|
||||
onSelectValue(value);
|
||||
}
|
||||
},
|
||||
style: {
|
||||
...contextStyles?.popup?.listItem,
|
||||
...style
|
||||
}
|
||||
}), /*#__PURE__*/React.createElement("div", {
|
||||
className: `${optionPrefixCls}-content`
|
||||
}, typeof optionRender === 'function' ? optionRender(item, {
|
||||
index: itemIndex
|
||||
}) : content), /*#__PURE__*/React.isValidElement(menuItemSelectedIcon) || selected, iconVisible && /*#__PURE__*/React.createElement(TransBtn, {
|
||||
className: `${itemPrefixCls}-option-state`,
|
||||
customizeIcon: menuItemSelectedIcon,
|
||||
customizeIconProps: {
|
||||
value,
|
||||
disabled: mergedDisabled,
|
||||
isSelected: selected
|
||||
}
|
||||
}, selected ? '✓' : null));
|
||||
}));
|
||||
};
|
||||
const RefOptionList = /*#__PURE__*/React.forwardRef(OptionList);
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
RefOptionList.displayName = 'OptionList';
|
||||
}
|
||||
export default RefOptionList;
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* To match accessibility requirement, we always provide an input in the component.
|
||||
* Other element will not set `tabIndex` to avoid `onBlur` sequence problem.
|
||||
* For focused select, we set `aria-live="polite"` to update the accessibility content.
|
||||
*
|
||||
* ref:
|
||||
* - keyboard: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role#Keyboard_interactions
|
||||
*
|
||||
* New api:
|
||||
* - listHeight
|
||||
* - listItemHeight
|
||||
* - component
|
||||
*
|
||||
* Remove deprecated api:
|
||||
* - multiple
|
||||
* - tags
|
||||
* - combobox
|
||||
* - firstActiveValue
|
||||
* - dropdownMenuStyle
|
||||
* - openClassName (Not list in api)
|
||||
*
|
||||
* Update:
|
||||
* - `backfill` only support `combobox` mode
|
||||
* - `combobox` mode not support `labelInValue` since it's meaningless
|
||||
* - `getInputElement` only support `combobox` mode
|
||||
* - `onChange` return OptionData instead of ReactNode
|
||||
* - `filterOption` `onChange` `onSelect` accept OptionData instead of ReactNode
|
||||
* - `combobox` mode trigger `onChange` will get `undefined` if no `value` match in Option
|
||||
* - `combobox` mode not support `optionLabelProp`
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import type { BaseSelectPropsWithoutPrivate, BaseSelectRef, BaseSelectSemanticName, DisplayValueType, RenderNode } from './BaseSelect';
|
||||
import OptGroup from './OptGroup';
|
||||
import Option from './Option';
|
||||
import type { FlattenOptionData } from './interface';
|
||||
export type OnActiveValue = (active: RawValueType, index: number, info?: {
|
||||
source?: 'keyboard' | 'mouse';
|
||||
}) => void;
|
||||
export type OnInternalSelect = (value: RawValueType, info: {
|
||||
selected: boolean;
|
||||
}) => void;
|
||||
export type RawValueType = string | number;
|
||||
export interface LabelInValueType {
|
||||
label: React.ReactNode;
|
||||
value: RawValueType;
|
||||
}
|
||||
export type DraftValueType = RawValueType | LabelInValueType | DisplayValueType | (RawValueType | LabelInValueType | DisplayValueType)[];
|
||||
export type FilterFunc<OptionType> = (inputValue: string, option?: OptionType) => boolean;
|
||||
export interface FieldNames {
|
||||
value?: string;
|
||||
label?: string;
|
||||
groupLabel?: string;
|
||||
options?: string;
|
||||
}
|
||||
export interface BaseOptionType {
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
title?: string;
|
||||
[name: string]: any;
|
||||
}
|
||||
export interface DefaultOptionType extends BaseOptionType {
|
||||
label?: React.ReactNode;
|
||||
value?: string | number | null;
|
||||
children?: Omit<DefaultOptionType, 'children'>[];
|
||||
}
|
||||
export type SelectHandler<ValueType, OptionType extends BaseOptionType = DefaultOptionType> = (value: ValueType, option: OptionType) => void;
|
||||
type ArrayElementType<T> = T extends (infer E)[] ? E : T;
|
||||
export type SemanticName = BaseSelectSemanticName;
|
||||
export type PopupSemantic = 'listItem' | 'list';
|
||||
export interface SearchConfig<OptionType> {
|
||||
searchValue?: string;
|
||||
autoClearSearchValue?: boolean;
|
||||
onSearch?: (value: string) => void;
|
||||
filterOption?: boolean | FilterFunc<OptionType>;
|
||||
filterSort?: (optionA: OptionType, optionB: OptionType, info: {
|
||||
searchValue: string;
|
||||
}) => number;
|
||||
optionFilterProp?: string | string[];
|
||||
}
|
||||
export interface SelectProps<ValueType = any, OptionType extends BaseOptionType = DefaultOptionType> extends Omit<BaseSelectPropsWithoutPrivate, 'showSearch'> {
|
||||
prefixCls?: string;
|
||||
id?: string;
|
||||
backfill?: boolean;
|
||||
fieldNames?: FieldNames;
|
||||
/** @deprecated please use showSearch.onSearch */
|
||||
onSearch?: SearchConfig<OptionType>['onSearch'];
|
||||
showSearch?: boolean | SearchConfig<OptionType>;
|
||||
/** @deprecated please use showSearch.searchValue */
|
||||
searchValue?: SearchConfig<OptionType>['searchValue'];
|
||||
/** @deprecated please use showSearch.autoClearSearchValue */
|
||||
autoClearSearchValue?: boolean;
|
||||
onSelect?: SelectHandler<ArrayElementType<ValueType>, OptionType>;
|
||||
onDeselect?: SelectHandler<ArrayElementType<ValueType>, OptionType>;
|
||||
onActive?: (value: ValueType) => void;
|
||||
/**
|
||||
* In Select, `false` means do nothing.
|
||||
* In TreeSelect, `false` will highlight match item.
|
||||
* It's by design.
|
||||
*/
|
||||
/** @deprecated please use showSearch.filterOption */
|
||||
filterOption?: SearchConfig<OptionType>['filterOption'];
|
||||
/** @deprecated please use showSearch.filterSort */
|
||||
filterSort?: SearchConfig<OptionType>['filterSort'];
|
||||
/** @deprecated please use showSearch.optionFilterProp */
|
||||
optionFilterProp?: string | string[];
|
||||
optionLabelProp?: string;
|
||||
children?: React.ReactNode;
|
||||
options?: OptionType[];
|
||||
optionRender?: (oriOption: FlattenOptionData<OptionType>, info: {
|
||||
index: number;
|
||||
}) => React.ReactNode;
|
||||
defaultActiveFirstOption?: boolean;
|
||||
virtual?: boolean;
|
||||
direction?: 'ltr' | 'rtl';
|
||||
listHeight?: number;
|
||||
listItemHeight?: number;
|
||||
labelRender?: (props: LabelInValueType) => React.ReactNode;
|
||||
menuItemSelectedIcon?: RenderNode;
|
||||
mode?: 'combobox' | 'multiple' | 'tags';
|
||||
labelInValue?: boolean;
|
||||
value?: ValueType | null;
|
||||
defaultValue?: ValueType | null;
|
||||
maxCount?: number;
|
||||
onChange?: (value: ValueType, option?: OptionType | OptionType[]) => void;
|
||||
classNames?: Partial<Record<SemanticName, string>>;
|
||||
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
|
||||
}
|
||||
declare const TypedSelect: (<ValueType = any, OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType>(props: React.PropsWithChildren<SelectProps<ValueType, OptionType>> & React.RefAttributes<BaseSelectRef>) => React.ReactElement) & {
|
||||
Option: typeof Option;
|
||||
OptGroup: typeof OptGroup;
|
||||
};
|
||||
export default TypedSelect;
|
||||
+525
@@ -0,0 +1,525 @@
|
||||
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
||||
/**
|
||||
* To match accessibility requirement, we always provide an input in the component.
|
||||
* Other element will not set `tabIndex` to avoid `onBlur` sequence problem.
|
||||
* For focused select, we set `aria-live="polite"` to update the accessibility content.
|
||||
*
|
||||
* ref:
|
||||
* - keyboard: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role#Keyboard_interactions
|
||||
*
|
||||
* New api:
|
||||
* - listHeight
|
||||
* - listItemHeight
|
||||
* - component
|
||||
*
|
||||
* Remove deprecated api:
|
||||
* - multiple
|
||||
* - tags
|
||||
* - combobox
|
||||
* - firstActiveValue
|
||||
* - dropdownMenuStyle
|
||||
* - openClassName (Not list in api)
|
||||
*
|
||||
* Update:
|
||||
* - `backfill` only support `combobox` mode
|
||||
* - `combobox` mode not support `labelInValue` since it's meaningless
|
||||
* - `getInputElement` only support `combobox` mode
|
||||
* - `onChange` return OptionData instead of ReactNode
|
||||
* - `filterOption` `onChange` `onSelect` accept OptionData instead of ReactNode
|
||||
* - `combobox` mode trigger `onChange` will get `undefined` if no `value` match in Option
|
||||
* - `combobox` mode not support `optionLabelProp`
|
||||
*/
|
||||
|
||||
import useControlledState from "@rc-component/util/es/hooks/useControlledState";
|
||||
import warning from "@rc-component/util/es/warning";
|
||||
import * as React from 'react';
|
||||
import BaseSelect, { isMultiple } from "./BaseSelect";
|
||||
import OptGroup from "./OptGroup";
|
||||
import Option from "./Option";
|
||||
import OptionList from "./OptionList";
|
||||
import SelectContext from "./SelectContext";
|
||||
import useCache from "./hooks/useCache";
|
||||
import useFilterOptions from "./hooks/useFilterOptions";
|
||||
import useId from "@rc-component/util/es/hooks/useId";
|
||||
import useOptions from "./hooks/useOptions";
|
||||
import useRefFunc from "./hooks/useRefFunc";
|
||||
import { hasValue, isComboNoValue, toArray } from "./utils/commonUtil";
|
||||
import { fillFieldNames, flattenOptions, injectPropsWithOption } from "./utils/valueUtil";
|
||||
import warningProps, { warningNullOptions } from "./utils/warningPropsUtil";
|
||||
import useSearchConfig from "./hooks/useSearchConfig";
|
||||
const OMIT_DOM_PROPS = ['inputValue'];
|
||||
function isRawValue(value) {
|
||||
return !value || typeof value !== 'object';
|
||||
}
|
||||
const Select = /*#__PURE__*/React.forwardRef((props, ref) => {
|
||||
const {
|
||||
id,
|
||||
mode,
|
||||
prefixCls = 'rc-select',
|
||||
backfill,
|
||||
fieldNames,
|
||||
// Search
|
||||
showSearch,
|
||||
searchValue: legacySearchValue,
|
||||
onSearch: legacyOnSearch,
|
||||
autoClearSearchValue: legacyAutoClearSearchValue,
|
||||
filterOption: legacyFilterOption,
|
||||
optionFilterProp: legacyOptionFilterProp,
|
||||
filterSort: legacyFilterSort,
|
||||
// Select
|
||||
onSelect,
|
||||
onDeselect,
|
||||
onActive,
|
||||
popupMatchSelectWidth = true,
|
||||
optionLabelProp,
|
||||
options,
|
||||
optionRender,
|
||||
children,
|
||||
defaultActiveFirstOption,
|
||||
menuItemSelectedIcon,
|
||||
virtual,
|
||||
direction,
|
||||
listHeight = 200,
|
||||
listItemHeight = 20,
|
||||
labelRender,
|
||||
// Value
|
||||
value,
|
||||
defaultValue,
|
||||
labelInValue,
|
||||
onChange,
|
||||
maxCount,
|
||||
classNames,
|
||||
styles,
|
||||
...restProps
|
||||
} = props;
|
||||
const searchProps = {
|
||||
searchValue: legacySearchValue,
|
||||
onSearch: legacyOnSearch,
|
||||
autoClearSearchValue: legacyAutoClearSearchValue,
|
||||
filterOption: legacyFilterOption,
|
||||
optionFilterProp: legacyOptionFilterProp,
|
||||
filterSort: legacyFilterSort
|
||||
};
|
||||
const [mergedShowSearch, searchConfig] = useSearchConfig(showSearch, searchProps, mode);
|
||||
const {
|
||||
filterOption,
|
||||
searchValue,
|
||||
optionFilterProp,
|
||||
filterSort,
|
||||
onSearch,
|
||||
autoClearSearchValue = true
|
||||
} = searchConfig;
|
||||
const normalizedOptionFilterProp = React.useMemo(() => {
|
||||
if (!optionFilterProp) return [];
|
||||
return Array.isArray(optionFilterProp) ? optionFilterProp : [optionFilterProp];
|
||||
}, [optionFilterProp]);
|
||||
const mergedId = useId(id);
|
||||
const multiple = isMultiple(mode);
|
||||
const childrenAsData = !!(!options && children);
|
||||
const mergedFilterOption = React.useMemo(() => {
|
||||
if (filterOption === undefined && mode === 'combobox') {
|
||||
return false;
|
||||
}
|
||||
return filterOption;
|
||||
}, [filterOption, mode]);
|
||||
|
||||
// ========================= FieldNames =========================
|
||||
const mergedFieldNames = React.useMemo(() => fillFieldNames(fieldNames, childrenAsData), /* eslint-disable react-hooks/exhaustive-deps */
|
||||
[
|
||||
// We stringify fieldNames to avoid unnecessary re-renders.
|
||||
JSON.stringify(fieldNames), childrenAsData]
|
||||
/* eslint-enable react-hooks/exhaustive-deps */);
|
||||
|
||||
// =========================== Search ===========================
|
||||
const [internalSearchValue, setSearchValue] = useControlledState('', searchValue);
|
||||
const mergedSearchValue = internalSearchValue || '';
|
||||
|
||||
// =========================== Option ===========================
|
||||
const parsedOptions = useOptions(options, children, mergedFieldNames, normalizedOptionFilterProp, optionLabelProp);
|
||||
const {
|
||||
valueOptions,
|
||||
labelOptions,
|
||||
options: mergedOptions
|
||||
} = parsedOptions;
|
||||
|
||||
// ========================= Wrap Value =========================
|
||||
const convert2LabelValues = React.useCallback(draftValues => {
|
||||
// Convert to array
|
||||
const valueList = toArray(draftValues);
|
||||
|
||||
// Convert to labelInValue type
|
||||
return valueList.map(val => {
|
||||
let rawValue;
|
||||
let rawLabel;
|
||||
let rawDisabled;
|
||||
let rawTitle;
|
||||
|
||||
// Fill label & value
|
||||
if (isRawValue(val)) {
|
||||
rawValue = val;
|
||||
} else {
|
||||
rawLabel = val.label;
|
||||
rawValue = val.value;
|
||||
}
|
||||
const option = valueOptions.get(rawValue);
|
||||
if (option) {
|
||||
// Fill missing props
|
||||
if (rawLabel === undefined) rawLabel = option?.[optionLabelProp || mergedFieldNames.label];
|
||||
rawDisabled = option?.disabled;
|
||||
rawTitle = option?.title;
|
||||
|
||||
// Warning if label not same as provided
|
||||
if (process.env.NODE_ENV !== 'production' && !optionLabelProp) {
|
||||
const optionLabel = option?.[mergedFieldNames.label];
|
||||
if (optionLabel !== undefined && ! /*#__PURE__*/React.isValidElement(optionLabel) && ! /*#__PURE__*/React.isValidElement(rawLabel) && optionLabel !== rawLabel) {
|
||||
warning(false, '`label` of `value` is not same as `label` in Select options.');
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: rawLabel,
|
||||
value: rawValue,
|
||||
key: rawValue,
|
||||
disabled: rawDisabled,
|
||||
title: rawTitle
|
||||
};
|
||||
});
|
||||
}, [mergedFieldNames, optionLabelProp, valueOptions]);
|
||||
|
||||
// =========================== Values ===========================
|
||||
const [internalValue, setInternalValue] = useControlledState(defaultValue, value);
|
||||
|
||||
// Merged value with LabelValueType
|
||||
const rawLabeledValues = React.useMemo(() => {
|
||||
const newInternalValue = multiple && internalValue === null ? [] : internalValue;
|
||||
const values = convert2LabelValues(newInternalValue);
|
||||
|
||||
// combobox no need save value when it's no value (exclude value equal 0)
|
||||
if (mode === 'combobox' && isComboNoValue(values[0]?.value)) {
|
||||
return [];
|
||||
}
|
||||
return values;
|
||||
}, [internalValue, convert2LabelValues, mode, multiple]);
|
||||
|
||||
// Fill label with cache to avoid option remove
|
||||
const [mergedValues, getMixedOption] = useCache(rawLabeledValues, valueOptions);
|
||||
const displayValues = React.useMemo(() => {
|
||||
// `null` need show as placeholder instead
|
||||
// https://github.com/ant-design/ant-design/issues/25057
|
||||
if (!mode && mergedValues.length === 1) {
|
||||
const firstValue = mergedValues[0];
|
||||
if (firstValue.value === null && (firstValue.label === null || firstValue.label === undefined)) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return mergedValues.map(item => ({
|
||||
...item,
|
||||
label: (typeof labelRender === 'function' ? labelRender(item) : item.label) ?? item.value
|
||||
}));
|
||||
}, [mode, mergedValues, labelRender]);
|
||||
|
||||
/** Convert `displayValues` to raw value type set */
|
||||
const rawValues = React.useMemo(() => new Set(mergedValues.map(val => val.value)), [mergedValues]);
|
||||
React.useEffect(() => {
|
||||
if (mode === 'combobox') {
|
||||
const strValue = mergedValues[0]?.value;
|
||||
setSearchValue(hasValue(strValue) ? String(strValue) : '');
|
||||
}
|
||||
}, [mergedValues]);
|
||||
|
||||
// ======================= Display Option =======================
|
||||
// Create a placeholder item if not exist in `options`
|
||||
const createTagOption = useRefFunc((val, label) => {
|
||||
const mergedLabel = label ?? val;
|
||||
return {
|
||||
[mergedFieldNames.value]: val,
|
||||
[mergedFieldNames.label]: mergedLabel
|
||||
};
|
||||
});
|
||||
|
||||
// Fill tag as option if mode is `tags`
|
||||
const filledTagOptions = React.useMemo(() => {
|
||||
if (mode !== 'tags') {
|
||||
return mergedOptions;
|
||||
}
|
||||
|
||||
// >>> Tag mode
|
||||
const cloneOptions = [...mergedOptions];
|
||||
|
||||
// Check if value exist in options (include new patch item)
|
||||
const existOptions = val => valueOptions.has(val);
|
||||
|
||||
// Fill current value as option
|
||||
[...mergedValues].sort((a, b) => a.value < b.value ? -1 : 1).forEach(item => {
|
||||
const val = item.value;
|
||||
if (!existOptions(val)) {
|
||||
cloneOptions.push(createTagOption(val, item.label));
|
||||
}
|
||||
});
|
||||
return cloneOptions;
|
||||
}, [createTagOption, mergedOptions, valueOptions, mergedValues, mode]);
|
||||
const filteredOptions = useFilterOptions(filledTagOptions, mergedFieldNames, mergedSearchValue, mergedFilterOption, normalizedOptionFilterProp);
|
||||
|
||||
// Fill options with search value if needed
|
||||
const filledSearchOptions = React.useMemo(() => {
|
||||
const hasItemMatchingSearch = item => {
|
||||
if (normalizedOptionFilterProp.length) {
|
||||
return normalizedOptionFilterProp.some(prop => item?.[prop] === mergedSearchValue);
|
||||
}
|
||||
return item?.value === mergedSearchValue;
|
||||
};
|
||||
if (mode !== 'tags' || !mergedSearchValue || filteredOptions.some(item => hasItemMatchingSearch(item))) {
|
||||
return filteredOptions;
|
||||
}
|
||||
// ignore when search value equal select input value
|
||||
if (filteredOptions.some(item => item[mergedFieldNames.value] === mergedSearchValue)) {
|
||||
return filteredOptions;
|
||||
}
|
||||
// Fill search value as option
|
||||
return [createTagOption(mergedSearchValue), ...filteredOptions];
|
||||
}, [createTagOption, normalizedOptionFilterProp, mode, filteredOptions, mergedSearchValue, mergedFieldNames]);
|
||||
const sorter = inputOptions => {
|
||||
const sortedOptions = [...inputOptions].sort((a, b) => filterSort(a, b, {
|
||||
searchValue: mergedSearchValue
|
||||
}));
|
||||
return sortedOptions.map(item => {
|
||||
if (Array.isArray(item.options)) {
|
||||
return {
|
||||
...item,
|
||||
options: item.options.length > 0 ? sorter(item.options) : item.options
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
};
|
||||
const orderedFilteredOptions = React.useMemo(() => {
|
||||
if (!filterSort) {
|
||||
return filledSearchOptions;
|
||||
}
|
||||
return sorter(filledSearchOptions);
|
||||
}, [filledSearchOptions, filterSort, mergedSearchValue]);
|
||||
const displayOptions = React.useMemo(() => flattenOptions(orderedFilteredOptions, {
|
||||
fieldNames: mergedFieldNames,
|
||||
childrenAsData
|
||||
}), [orderedFilteredOptions, mergedFieldNames, childrenAsData]);
|
||||
|
||||
// =========================== Change ===========================
|
||||
const triggerChange = values => {
|
||||
const labeledValues = convert2LabelValues(values);
|
||||
setInternalValue(labeledValues);
|
||||
if (onChange && (
|
||||
// Trigger event only when value changed
|
||||
labeledValues.length !== mergedValues.length || labeledValues.some((newVal, index) => mergedValues[index]?.value !== newVal?.value))) {
|
||||
const returnValues = labelInValue ? labeledValues.map(({
|
||||
label: l,
|
||||
value: v
|
||||
}) => ({
|
||||
label: l,
|
||||
value: v
|
||||
})) : labeledValues.map(v => v.value);
|
||||
const returnOptions = labeledValues.map(v => injectPropsWithOption(getMixedOption(v.value)));
|
||||
onChange(
|
||||
// Value
|
||||
multiple ? returnValues : returnValues[0],
|
||||
// Option
|
||||
multiple ? returnOptions : returnOptions[0]);
|
||||
}
|
||||
};
|
||||
|
||||
// ======================= Accessibility ========================
|
||||
const [activeValue, setActiveValue] = React.useState(null);
|
||||
const [accessibilityIndex, setAccessibilityIndex] = React.useState(0);
|
||||
const mergedDefaultActiveFirstOption = defaultActiveFirstOption !== undefined ? defaultActiveFirstOption : mode !== 'combobox';
|
||||
const activeEventRef = React.useRef();
|
||||
const onActiveValue = React.useCallback((active, index, {
|
||||
source = 'keyboard'
|
||||
} = {}) => {
|
||||
setAccessibilityIndex(index);
|
||||
if (backfill && mode === 'combobox' && active !== null && source === 'keyboard') {
|
||||
setActiveValue(String(active));
|
||||
}
|
||||
|
||||
// Active will call multiple times.
|
||||
// We only need trigger the last one.
|
||||
const promise = Promise.resolve().then(() => {
|
||||
if (activeEventRef.current === promise) {
|
||||
onActive?.(active);
|
||||
}
|
||||
});
|
||||
activeEventRef.current = promise;
|
||||
}, [backfill, mode, onActive]);
|
||||
|
||||
// ========================= OptionList =========================
|
||||
const triggerSelect = (val, selected, type) => {
|
||||
const getSelectEnt = () => {
|
||||
const option = getMixedOption(val);
|
||||
return [labelInValue ? {
|
||||
label: option?.[mergedFieldNames.label],
|
||||
value: val
|
||||
} : val, injectPropsWithOption(option)];
|
||||
};
|
||||
if (selected && onSelect) {
|
||||
const [wrappedValue, option] = getSelectEnt();
|
||||
onSelect(wrappedValue, option);
|
||||
} else if (!selected && onDeselect && type !== 'clear') {
|
||||
const [wrappedValue, option] = getSelectEnt();
|
||||
onDeselect(wrappedValue, option);
|
||||
}
|
||||
};
|
||||
|
||||
// Used for OptionList selection
|
||||
const onInternalSelect = useRefFunc((val, info) => {
|
||||
let cloneValues;
|
||||
|
||||
// Single mode always trigger select only with option list
|
||||
const mergedSelect = multiple ? info.selected : true;
|
||||
if (mergedSelect) {
|
||||
cloneValues = multiple ? [...mergedValues, val] : [val];
|
||||
} else {
|
||||
cloneValues = mergedValues.filter(v => v.value !== val);
|
||||
}
|
||||
triggerChange(cloneValues);
|
||||
triggerSelect(val, mergedSelect);
|
||||
|
||||
// Clean search value if single or configured
|
||||
if (mode === 'combobox') {
|
||||
setActiveValue('');
|
||||
} else if (!isMultiple || autoClearSearchValue) {
|
||||
setSearchValue('');
|
||||
setActiveValue('');
|
||||
}
|
||||
});
|
||||
|
||||
// ======================= Display Change =======================
|
||||
// BaseSelect display values change
|
||||
const onDisplayValuesChange = (nextValues, info) => {
|
||||
triggerChange(nextValues);
|
||||
const {
|
||||
type,
|
||||
values
|
||||
} = info;
|
||||
if (type === 'remove' || type === 'clear') {
|
||||
values.forEach(item => {
|
||||
triggerSelect(item.value, false, type);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// =========================== Search ===========================
|
||||
const onInternalSearch = (searchText, info) => {
|
||||
setSearchValue(searchText);
|
||||
setActiveValue(null);
|
||||
|
||||
// [Submit] Tag mode should flush input
|
||||
if (info.source === 'submit') {
|
||||
const formatted = (searchText || '').trim();
|
||||
// prevent empty tags from appearing when you click the Enter button
|
||||
if (formatted) {
|
||||
const newRawValues = Array.from(new Set([...rawValues, formatted]));
|
||||
triggerChange(newRawValues);
|
||||
triggerSelect(formatted, true);
|
||||
setSearchValue('');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (info.source !== 'blur') {
|
||||
if (mode === 'combobox') {
|
||||
triggerChange(searchText);
|
||||
}
|
||||
onSearch?.(searchText);
|
||||
}
|
||||
};
|
||||
const onInternalSearchSplit = words => {
|
||||
let patchValues = words;
|
||||
if (mode !== 'tags') {
|
||||
patchValues = words.map(word => {
|
||||
const opt = labelOptions.get(word);
|
||||
return opt?.value;
|
||||
}).filter(val => val !== undefined);
|
||||
}
|
||||
const newRawValues = Array.from(new Set([...rawValues, ...patchValues]));
|
||||
triggerChange(newRawValues);
|
||||
newRawValues.forEach(newRawValue => {
|
||||
triggerSelect(newRawValue, true);
|
||||
});
|
||||
};
|
||||
|
||||
// ========================== Context ===========================
|
||||
const selectContext = React.useMemo(() => {
|
||||
const realVirtual = virtual !== false && popupMatchSelectWidth !== false;
|
||||
return {
|
||||
...parsedOptions,
|
||||
flattenOptions: displayOptions,
|
||||
onActiveValue,
|
||||
defaultActiveFirstOption: mergedDefaultActiveFirstOption,
|
||||
onSelect: onInternalSelect,
|
||||
menuItemSelectedIcon,
|
||||
rawValues,
|
||||
fieldNames: mergedFieldNames,
|
||||
virtual: realVirtual,
|
||||
direction,
|
||||
listHeight,
|
||||
listItemHeight,
|
||||
childrenAsData,
|
||||
maxCount,
|
||||
optionRender,
|
||||
classNames,
|
||||
styles
|
||||
};
|
||||
}, [maxCount, parsedOptions, displayOptions, onActiveValue, mergedDefaultActiveFirstOption, onInternalSelect, menuItemSelectedIcon, rawValues, mergedFieldNames, virtual, popupMatchSelectWidth, direction, listHeight, listItemHeight, childrenAsData, optionRender, classNames, styles]);
|
||||
|
||||
// ========================== Warning ===========================
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
warningProps(props);
|
||||
warningNullOptions(mergedOptions, mergedFieldNames);
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// == Render ==
|
||||
// ==============================================================
|
||||
return /*#__PURE__*/React.createElement(SelectContext.Provider, {
|
||||
value: selectContext
|
||||
}, /*#__PURE__*/React.createElement(BaseSelect, _extends({}, restProps, {
|
||||
// >>> MISC
|
||||
id: mergedId,
|
||||
prefixCls: prefixCls,
|
||||
ref: ref,
|
||||
omitDomProps: OMIT_DOM_PROPS,
|
||||
mode: mode
|
||||
// >>> Style
|
||||
,
|
||||
classNames: classNames,
|
||||
styles: styles
|
||||
// >>> Values
|
||||
,
|
||||
displayValues: displayValues,
|
||||
onDisplayValuesChange: onDisplayValuesChange,
|
||||
maxCount: maxCount
|
||||
// >>> Trigger
|
||||
,
|
||||
direction: direction
|
||||
// >>> Search
|
||||
,
|
||||
showSearch: mergedShowSearch,
|
||||
searchValue: mergedSearchValue,
|
||||
onSearch: onInternalSearch,
|
||||
autoClearSearchValue: autoClearSearchValue,
|
||||
onSearchSplit: onInternalSearchSplit,
|
||||
popupMatchSelectWidth: popupMatchSelectWidth
|
||||
// >>> OptionList
|
||||
,
|
||||
OptionList: OptionList,
|
||||
emptyOptions: !displayOptions.length
|
||||
// >>> Accessibility
|
||||
,
|
||||
activeValue: activeValue,
|
||||
activeDescendantId: `${mergedId}_list_${accessibilityIndex}`
|
||||
})));
|
||||
});
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Select.displayName = 'Select';
|
||||
}
|
||||
const TypedSelect = Select;
|
||||
TypedSelect.Option = Option;
|
||||
TypedSelect.OptGroup = OptGroup;
|
||||
export default TypedSelect;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react';
|
||||
import type { RawValueType, RenderNode } from './BaseSelect';
|
||||
import type { BaseOptionType, FieldNames, OnActiveValue, OnInternalSelect, SelectProps, SemanticName, PopupSemantic } from './Select';
|
||||
import type { FlattenOptionData } from './interface';
|
||||
/**
|
||||
* SelectContext is only used for Select. BaseSelect should not consume this context.
|
||||
*/
|
||||
export interface SelectContextProps {
|
||||
classNames?: Partial<Record<SemanticName, string>> & {
|
||||
popup?: Partial<Record<PopupSemantic, string>>;
|
||||
};
|
||||
styles?: Partial<Record<SemanticName, React.CSSProperties>> & {
|
||||
popup?: Partial<Record<PopupSemantic, React.CSSProperties>>;
|
||||
};
|
||||
options: BaseOptionType[];
|
||||
optionRender?: SelectProps['optionRender'];
|
||||
flattenOptions: FlattenOptionData<BaseOptionType>[];
|
||||
onActiveValue: OnActiveValue;
|
||||
defaultActiveFirstOption?: boolean;
|
||||
onSelect: OnInternalSelect;
|
||||
menuItemSelectedIcon?: RenderNode;
|
||||
rawValues: Set<RawValueType>;
|
||||
fieldNames?: FieldNames;
|
||||
virtual?: boolean;
|
||||
direction?: 'ltr' | 'rtl';
|
||||
listHeight?: number;
|
||||
listItemHeight?: number;
|
||||
childrenAsData?: boolean;
|
||||
maxCount?: number;
|
||||
}
|
||||
declare const SelectContext: React.Context<SelectContextProps>;
|
||||
export default SelectContext;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import * as React from 'react';
|
||||
|
||||
// Use any here since we do not get the type during compilation
|
||||
/**
|
||||
* SelectContext is only used for Select. BaseSelect should not consume this context.
|
||||
*/
|
||||
|
||||
const SelectContext = /*#__PURE__*/React.createContext(null);
|
||||
export default SelectContext;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import * as React from 'react';
|
||||
export interface AffixProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
export default function Affix(props: AffixProps): React.JSX.Element;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import * as React from 'react';
|
||||
// Affix is a simple wrapper which should not read context or logical props
|
||||
export default function Affix(props) {
|
||||
const {
|
||||
children,
|
||||
...restProps
|
||||
} = props;
|
||||
if (!children) {
|
||||
return null;
|
||||
}
|
||||
return /*#__PURE__*/React.createElement("div", restProps, children);
|
||||
}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import type { SharedContentProps } from '.';
|
||||
declare const _default: React.ForwardRefExoticComponent<SharedContentProps & React.RefAttributes<HTMLInputElement>>;
|
||||
export default _default;
|
||||
Generated
Vendored
+158
@@ -0,0 +1,158 @@
|
||||
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
||||
import * as React from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import Overflow from '@rc-component/overflow';
|
||||
import Input from "../Input";
|
||||
import { useSelectInputContext } from "../context";
|
||||
import TransBtn from "../../TransBtn";
|
||||
import { getTitle } from "../../utils/commonUtil";
|
||||
import useBaseProps from "../../hooks/useBaseProps";
|
||||
import Placeholder from "./Placeholder";
|
||||
function itemKey(value) {
|
||||
return value.key ?? value.value;
|
||||
}
|
||||
const onPreventMouseDown = event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
export default /*#__PURE__*/React.forwardRef(function MultipleContent({
|
||||
inputProps
|
||||
}, ref) {
|
||||
const {
|
||||
prefixCls,
|
||||
displayValues,
|
||||
searchValue,
|
||||
mode,
|
||||
onSelectorRemove,
|
||||
removeIcon: removeIconFromContext
|
||||
} = useSelectInputContext();
|
||||
const {
|
||||
disabled,
|
||||
showSearch,
|
||||
triggerOpen,
|
||||
rawOpen,
|
||||
toggleOpen,
|
||||
autoClearSearchValue,
|
||||
tagRender: tagRenderFromContext,
|
||||
maxTagPlaceholder: maxTagPlaceholderFromContext,
|
||||
maxTagTextLength,
|
||||
maxTagCount,
|
||||
classNames,
|
||||
styles
|
||||
} = useBaseProps();
|
||||
const selectionItemPrefixCls = `${prefixCls}-selection-item`;
|
||||
|
||||
// ===================== Search ======================
|
||||
// Apply autoClearSearchValue logic: when dropdown is closed and autoClearSearchValue is not false (default true), clear search value
|
||||
// Use rawOpen to avoid clearing search when emptyListContent blocks open
|
||||
let computedSearchValue = searchValue;
|
||||
if (!rawOpen && mode === 'multiple' && autoClearSearchValue !== false) {
|
||||
computedSearchValue = '';
|
||||
}
|
||||
const inputValue = showSearch ? computedSearchValue || '' : '';
|
||||
const inputEditable = showSearch && !disabled;
|
||||
|
||||
// Props from context with safe defaults
|
||||
const removeIcon = removeIconFromContext ?? '×';
|
||||
const maxTagPlaceholder = maxTagPlaceholderFromContext ?? (omittedValues => `+ ${omittedValues.length} ...`);
|
||||
const tagRender = tagRenderFromContext;
|
||||
const onToggleOpen = newOpen => {
|
||||
toggleOpen(newOpen);
|
||||
};
|
||||
const onRemove = value => {
|
||||
onSelectorRemove?.(value);
|
||||
};
|
||||
|
||||
// ======================== Item ========================
|
||||
// >>> Render Selector Node. Includes Item & Rest
|
||||
const defaultRenderSelector = (item, content, itemDisabled, closable, onClose) => /*#__PURE__*/React.createElement("span", {
|
||||
title: getTitle(item),
|
||||
className: clsx(selectionItemPrefixCls, {
|
||||
[`${selectionItemPrefixCls}-disabled`]: itemDisabled
|
||||
}, classNames?.item),
|
||||
style: styles?.item
|
||||
}, /*#__PURE__*/React.createElement("span", {
|
||||
className: clsx(`${selectionItemPrefixCls}-content`, classNames?.itemContent),
|
||||
style: styles?.itemContent
|
||||
}, content), closable && /*#__PURE__*/React.createElement(TransBtn, {
|
||||
className: clsx(`${selectionItemPrefixCls}-remove`, classNames?.itemRemove),
|
||||
style: styles?.itemRemove,
|
||||
onMouseDown: onPreventMouseDown,
|
||||
onClick: onClose,
|
||||
customizeIcon: removeIcon
|
||||
}, "\xD7"));
|
||||
const customizeRenderSelector = (value, content, itemDisabled, closable, onClose, isMaxTag, info) => {
|
||||
const onMouseDown = e => {
|
||||
onPreventMouseDown(e);
|
||||
onToggleOpen(!triggerOpen);
|
||||
};
|
||||
return /*#__PURE__*/React.createElement("span", {
|
||||
onMouseDown: onMouseDown
|
||||
}, tagRender({
|
||||
label: content,
|
||||
value,
|
||||
index: info?.index,
|
||||
disabled: itemDisabled,
|
||||
closable,
|
||||
onClose,
|
||||
isMaxTag: !!isMaxTag
|
||||
}));
|
||||
};
|
||||
|
||||
// ====================== Overflow ======================
|
||||
const renderItem = (valueItem, info) => {
|
||||
const {
|
||||
disabled: itemDisabled,
|
||||
label,
|
||||
value
|
||||
} = valueItem;
|
||||
const closable = !disabled && !itemDisabled;
|
||||
let displayLabel = label;
|
||||
if (typeof maxTagTextLength === 'number') {
|
||||
if (typeof label === 'string' || typeof label === 'number') {
|
||||
const strLabel = String(displayLabel);
|
||||
if (strLabel.length > maxTagTextLength) {
|
||||
displayLabel = `${strLabel.slice(0, maxTagTextLength)}...`;
|
||||
}
|
||||
}
|
||||
}
|
||||
const onClose = event => {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
onRemove(valueItem);
|
||||
};
|
||||
return typeof tagRender === 'function' ? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose, undefined, info) : defaultRenderSelector(valueItem, displayLabel, itemDisabled, closable, onClose);
|
||||
};
|
||||
const renderRest = omittedValues => {
|
||||
// https://github.com/ant-design/ant-design/issues/48930
|
||||
if (!displayValues.length) {
|
||||
return null;
|
||||
}
|
||||
const content = typeof maxTagPlaceholder === 'function' ? maxTagPlaceholder(omittedValues) : maxTagPlaceholder;
|
||||
return typeof tagRender === 'function' ? customizeRenderSelector(undefined, content, false, false, undefined, true) : defaultRenderSelector({
|
||||
title: content
|
||||
}, content, false);
|
||||
};
|
||||
|
||||
// ======================= Render =======================
|
||||
return /*#__PURE__*/React.createElement(Overflow, {
|
||||
prefixCls: `${prefixCls}-content`,
|
||||
className: classNames?.content,
|
||||
style: styles?.content,
|
||||
prefix: !displayValues.length && !inputValue && /*#__PURE__*/React.createElement(Placeholder, null),
|
||||
data: displayValues,
|
||||
renderItem: renderItem,
|
||||
renderRest: renderRest,
|
||||
suffix: /*#__PURE__*/React.createElement(Input, _extends({
|
||||
ref: ref,
|
||||
disabled: disabled,
|
||||
readOnly: !inputEditable
|
||||
}, inputProps, {
|
||||
value: inputValue || '',
|
||||
syncWidth: true
|
||||
})),
|
||||
itemKey: itemKey,
|
||||
maxCount: maxTagCount
|
||||
});
|
||||
});
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import * as React from 'react';
|
||||
export interface PlaceholderProps {
|
||||
show?: boolean;
|
||||
}
|
||||
export default function Placeholder(props: PlaceholderProps): React.JSX.Element;
|
||||
Generated
Vendored
+28
@@ -0,0 +1,28 @@
|
||||
import * as React from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import { useSelectInputContext } from "../context";
|
||||
import useBaseProps from "../../hooks/useBaseProps";
|
||||
export default function Placeholder(props) {
|
||||
const {
|
||||
prefixCls,
|
||||
placeholder,
|
||||
displayValues
|
||||
} = useSelectInputContext();
|
||||
const {
|
||||
classNames,
|
||||
styles
|
||||
} = useBaseProps();
|
||||
const {
|
||||
show = true
|
||||
} = props;
|
||||
if (displayValues.length) {
|
||||
return null;
|
||||
}
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
className: clsx(`${prefixCls}-placeholder`, classNames?.placeholder),
|
||||
style: {
|
||||
visibility: show ? 'visible' : 'hidden',
|
||||
...styles?.placeholder
|
||||
}
|
||||
}, placeholder);
|
||||
}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import type { SharedContentProps } from '.';
|
||||
declare const SingleContent: React.ForwardRefExoticComponent<SharedContentProps & React.RefAttributes<HTMLInputElement>>;
|
||||
export default SingleContent;
|
||||
Generated
Vendored
+102
@@ -0,0 +1,102 @@
|
||||
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
||||
import * as React from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import Input from "../Input";
|
||||
import { useSelectInputContext } from "../context";
|
||||
import useBaseProps from "../../hooks/useBaseProps";
|
||||
import Placeholder from "./Placeholder";
|
||||
import SelectContext from "../../SelectContext";
|
||||
import { getTitle } from "../../utils/commonUtil";
|
||||
const SingleContent = /*#__PURE__*/React.forwardRef(({
|
||||
inputProps
|
||||
}, ref) => {
|
||||
const {
|
||||
prefixCls,
|
||||
searchValue,
|
||||
activeValue,
|
||||
displayValues,
|
||||
maxLength,
|
||||
mode,
|
||||
components
|
||||
} = useSelectInputContext();
|
||||
const {
|
||||
triggerOpen,
|
||||
title: rootTitle,
|
||||
showSearch,
|
||||
classNames,
|
||||
styles
|
||||
} = useBaseProps();
|
||||
const selectContext = React.useContext(SelectContext);
|
||||
const [inputChanged, setInputChanged] = React.useState(false);
|
||||
const combobox = mode === 'combobox';
|
||||
const displayValue = displayValues[0];
|
||||
|
||||
// Implement the same logic as the old SingleSelector
|
||||
const mergedSearchValue = React.useMemo(() => {
|
||||
if (combobox && activeValue && !inputChanged && triggerOpen) {
|
||||
return activeValue;
|
||||
}
|
||||
return showSearch ? searchValue : '';
|
||||
}, [combobox, activeValue, inputChanged, triggerOpen, searchValue, showSearch]);
|
||||
const [optionClassName, optionStyle, optionTitle, hasOptionStyle] = React.useMemo(() => {
|
||||
let className;
|
||||
let style;
|
||||
let titleValue;
|
||||
if (displayValue && selectContext?.flattenOptions) {
|
||||
const option = selectContext.flattenOptions.find(opt => opt.value === displayValue.value);
|
||||
if (option?.data) {
|
||||
className = option.data.className;
|
||||
style = option.data.style;
|
||||
titleValue = getTitle(option.data);
|
||||
}
|
||||
}
|
||||
if (displayValue && !titleValue) {
|
||||
titleValue = getTitle(displayValue);
|
||||
}
|
||||
if (rootTitle !== undefined) {
|
||||
titleValue = rootTitle;
|
||||
}
|
||||
const nextHasStyle = !!className || !!style;
|
||||
return [className, style, titleValue, nextHasStyle];
|
||||
}, [displayValue, selectContext?.flattenOptions, rootTitle]);
|
||||
React.useEffect(() => {
|
||||
if (combobox) {
|
||||
setInputChanged(false);
|
||||
}
|
||||
}, [combobox, activeValue]);
|
||||
|
||||
// ========================== Render ==========================
|
||||
const showHasValueCls = displayValue && displayValue.label !== null && displayValue.label !== undefined && String(displayValue.label).trim() !== '';
|
||||
|
||||
// Render value
|
||||
// Only render value when not using custom input in combobox mode
|
||||
const shouldRenderValue = !(combobox && components?.input);
|
||||
const renderValue = shouldRenderValue ? displayValue ? hasOptionStyle ? /*#__PURE__*/React.createElement("div", {
|
||||
className: clsx(`${prefixCls}-content-value`, optionClassName),
|
||||
style: {
|
||||
...(mergedSearchValue ? {
|
||||
visibility: 'hidden'
|
||||
} : {}),
|
||||
...optionStyle
|
||||
},
|
||||
title: optionTitle
|
||||
}, displayValue.label) : displayValue.label : /*#__PURE__*/React.createElement(Placeholder, {
|
||||
show: !mergedSearchValue
|
||||
}) : null;
|
||||
// Render
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
className: clsx(`${prefixCls}-content`, showHasValueCls && `${prefixCls}-content-has-value`, mergedSearchValue && `${prefixCls}-content-has-search-value`, hasOptionStyle && `${prefixCls}-content-has-option-style`, classNames?.content),
|
||||
style: styles?.content,
|
||||
title: hasOptionStyle ? undefined : optionTitle
|
||||
}, renderValue, /*#__PURE__*/React.createElement(Input, _extends({
|
||||
ref: ref
|
||||
}, inputProps, {
|
||||
value: mergedSearchValue,
|
||||
maxLength: mode === 'combobox' ? maxLength : undefined,
|
||||
onChange: e => {
|
||||
setInputChanged(true);
|
||||
inputProps.onChange?.(e);
|
||||
}
|
||||
})));
|
||||
});
|
||||
export default SingleContent;
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
import * as React from 'react';
|
||||
export interface SharedContentProps {
|
||||
inputProps: React.InputHTMLAttributes<HTMLInputElement>;
|
||||
}
|
||||
declare const SelectContent: React.ForwardRefExoticComponent<React.RefAttributes<HTMLInputElement>>;
|
||||
export default SelectContent;
|
||||
Generated
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import pickAttrs from "@rc-component/util/es/pickAttrs";
|
||||
import SingleContent from "./SingleContent";
|
||||
import MultipleContent from "./MultipleContent";
|
||||
import { useSelectInputContext } from "../context";
|
||||
import useBaseProps from "../../hooks/useBaseProps";
|
||||
const SelectContent = /*#__PURE__*/React.forwardRef(function SelectContent(_, ref) {
|
||||
const {
|
||||
multiple,
|
||||
onInputKeyDown,
|
||||
tabIndex
|
||||
} = useSelectInputContext();
|
||||
const baseProps = useBaseProps();
|
||||
const {
|
||||
showSearch
|
||||
} = baseProps;
|
||||
const ariaProps = pickAttrs(baseProps, {
|
||||
aria: true
|
||||
});
|
||||
const sharedInputProps = {
|
||||
...ariaProps,
|
||||
onKeyDown: onInputKeyDown,
|
||||
readOnly: !showSearch,
|
||||
tabIndex
|
||||
};
|
||||
if (multiple) {
|
||||
return /*#__PURE__*/React.createElement(MultipleContent, {
|
||||
ref: ref,
|
||||
inputProps: sharedInputProps
|
||||
});
|
||||
}
|
||||
return /*#__PURE__*/React.createElement(SingleContent, {
|
||||
ref: ref,
|
||||
inputProps: sharedInputProps
|
||||
});
|
||||
});
|
||||
export default SelectContent;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import * as React from 'react';
|
||||
export interface InputProps {
|
||||
id?: string;
|
||||
readOnly?: boolean;
|
||||
value?: string;
|
||||
onChange?: React.ChangeEventHandler<HTMLInputElement>;
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
|
||||
onFocus?: React.FocusEventHandler<HTMLInputElement>;
|
||||
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
maxLength?: number;
|
||||
/** width always match content width */
|
||||
syncWidth?: boolean;
|
||||
/** autoComplete for input */
|
||||
autoComplete?: string;
|
||||
}
|
||||
declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<HTMLInputElement>>;
|
||||
export default Input;
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
import * as React from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
import { useSelectInputContext } from "./context";
|
||||
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
|
||||
import useBaseProps from "../hooks/useBaseProps";
|
||||
import { composeRef } from "@rc-component/util/es/ref";
|
||||
const Input = /*#__PURE__*/React.forwardRef((props, ref) => {
|
||||
const {
|
||||
onChange,
|
||||
onKeyDown,
|
||||
onBlur,
|
||||
style,
|
||||
syncWidth,
|
||||
value,
|
||||
className,
|
||||
autoComplete,
|
||||
...restProps
|
||||
} = props;
|
||||
const {
|
||||
prefixCls,
|
||||
mode,
|
||||
onSearch,
|
||||
onSearchSubmit,
|
||||
onInputBlur,
|
||||
autoFocus,
|
||||
tokenWithEnter,
|
||||
placeholder,
|
||||
components: {
|
||||
input: InputComponent = 'input'
|
||||
}
|
||||
} = useSelectInputContext();
|
||||
const {
|
||||
id,
|
||||
classNames,
|
||||
styles,
|
||||
open,
|
||||
activeDescendantId,
|
||||
role,
|
||||
disabled
|
||||
} = useBaseProps() || {};
|
||||
const inputCls = clsx(`${prefixCls}-input`, classNames?.input, className);
|
||||
|
||||
// Used to handle input method composition status
|
||||
const compositionStatusRef = React.useRef(false);
|
||||
|
||||
// Used to handle paste content, similar to original Selector implementation
|
||||
const pastedTextRef = React.useRef(null);
|
||||
|
||||
// ============================== Refs ==============================
|
||||
const inputRef = React.useRef(null);
|
||||
React.useImperativeHandle(ref, () => inputRef.current);
|
||||
|
||||
// ============================== Data ==============================
|
||||
// Handle input changes
|
||||
const handleChange = event => {
|
||||
let {
|
||||
value: nextVal
|
||||
} = event.target;
|
||||
|
||||
// Handle pasted text with tokenWithEnter, similar to original Selector implementation
|
||||
if (tokenWithEnter && pastedTextRef.current && /[\r\n]/.test(pastedTextRef.current)) {
|
||||
// CRLF will be treated as a single space for input element
|
||||
const replacedText = pastedTextRef.current.replace(/[\r\n]+$/, '').replace(/\r\n/g, ' ').replace(/[\r\n]/g, ' ');
|
||||
nextVal = nextVal.replace(replacedText, pastedTextRef.current);
|
||||
}
|
||||
|
||||
// Reset pasted text reference
|
||||
pastedTextRef.current = null;
|
||||
|
||||
// Call onSearch callback
|
||||
if (onSearch) {
|
||||
onSearch(nextVal, true, compositionStatusRef.current);
|
||||
}
|
||||
|
||||
// Call original onChange callback
|
||||
onChange?.(event);
|
||||
};
|
||||
|
||||
// ============================ Keyboard ============================
|
||||
// Handle keyboard events
|
||||
const handleKeyDown = event => {
|
||||
const {
|
||||
key
|
||||
} = event;
|
||||
const {
|
||||
value: nextVal
|
||||
} = event.currentTarget;
|
||||
|
||||
// Handle Enter key submission - referencing Selector implementation
|
||||
if (key === 'Enter' && mode === 'tags' && !open && !compositionStatusRef.current && onSearchSubmit) {
|
||||
onSearchSubmit(nextVal);
|
||||
}
|
||||
|
||||
// Call original onKeyDown callback
|
||||
onKeyDown?.(event);
|
||||
};
|
||||
|
||||
// Handle blur events
|
||||
const handleBlur = event => {
|
||||
// Call onInputBlur callback
|
||||
onInputBlur?.();
|
||||
|
||||
// Call original onBlur callback
|
||||
onBlur?.(event);
|
||||
};
|
||||
|
||||
// Handle input method composition start
|
||||
const handleCompositionStart = () => {
|
||||
compositionStatusRef.current = true;
|
||||
};
|
||||
|
||||
// Handle input method composition end
|
||||
const handleCompositionEnd = event => {
|
||||
compositionStatusRef.current = false;
|
||||
|
||||
// Trigger search when input method composition ends, similar to original Selector
|
||||
if (mode !== 'combobox') {
|
||||
const {
|
||||
value: nextVal
|
||||
} = event.currentTarget;
|
||||
onSearch?.(nextVal, true, false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle paste events to track pasted content
|
||||
const handlePaste = event => {
|
||||
const {
|
||||
clipboardData
|
||||
} = event;
|
||||
const pastedValue = clipboardData?.getData('text');
|
||||
pastedTextRef.current = pastedValue || '';
|
||||
};
|
||||
|
||||
// ============================= Width ==============================
|
||||
const [widthCssVar, setWidthCssVar] = React.useState(undefined);
|
||||
|
||||
// When syncWidth is enabled, adjust input width based on content
|
||||
useLayoutEffect(() => {
|
||||
const input = inputRef.current;
|
||||
if (syncWidth && input) {
|
||||
input.style.width = '0px';
|
||||
const scrollWidth = input.scrollWidth;
|
||||
setWidthCssVar(scrollWidth);
|
||||
|
||||
// Reset input style
|
||||
input.style.width = '';
|
||||
}
|
||||
}, [syncWidth, value]);
|
||||
|
||||
// ============================= Render =============================
|
||||
// Extract shared input props
|
||||
const sharedInputProps = {
|
||||
id,
|
||||
type: mode === 'combobox' ? 'text' : 'search',
|
||||
...restProps,
|
||||
ref: inputRef,
|
||||
style: {
|
||||
...styles?.input,
|
||||
...style,
|
||||
'--select-input-width': widthCssVar
|
||||
},
|
||||
autoFocus,
|
||||
autoComplete: autoComplete || 'off',
|
||||
className: inputCls,
|
||||
disabled,
|
||||
value: value || '',
|
||||
onChange: handleChange,
|
||||
onKeyDown: handleKeyDown,
|
||||
onBlur: handleBlur,
|
||||
onPaste: handlePaste,
|
||||
onCompositionStart: handleCompositionStart,
|
||||
onCompositionEnd: handleCompositionEnd,
|
||||
// Accessibility attributes
|
||||
role: role || 'combobox',
|
||||
'aria-expanded': open || false,
|
||||
'aria-haspopup': 'listbox',
|
||||
'aria-owns': open ? `${id}_list` : undefined,
|
||||
'aria-autocomplete': 'list',
|
||||
'aria-controls': open ? `${id}_list` : undefined,
|
||||
'aria-activedescendant': open ? activeDescendantId : undefined
|
||||
};
|
||||
|
||||
// Handle different InputComponent types
|
||||
if ( /*#__PURE__*/React.isValidElement(InputComponent)) {
|
||||
// If InputComponent is a ReactElement, use cloneElement with merged props
|
||||
const existingProps = InputComponent.props || {};
|
||||
|
||||
// Start with shared props as base
|
||||
const mergedProps = {
|
||||
placeholder: props.placeholder || placeholder,
|
||||
...sharedInputProps,
|
||||
...existingProps
|
||||
};
|
||||
|
||||
// Batch update function calls
|
||||
Object.keys(existingProps).forEach(key => {
|
||||
const existingValue = existingProps[key];
|
||||
if (typeof existingValue === 'function') {
|
||||
// Merge event handlers
|
||||
mergedProps[key] = (...args) => {
|
||||
existingValue(...args);
|
||||
sharedInputProps[key]?.(...args);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Update ref
|
||||
mergedProps.ref = composeRef(InputComponent.ref, sharedInputProps.ref);
|
||||
return /*#__PURE__*/React.cloneElement(InputComponent, mergedProps);
|
||||
}
|
||||
|
||||
// If InputComponent is a component type, render normally
|
||||
const Component = InputComponent;
|
||||
return /*#__PURE__*/React.createElement(Component, sharedInputProps);
|
||||
});
|
||||
export default Input;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import type { SelectInputProps } from '.';
|
||||
export type ContentContextProps = SelectInputProps;
|
||||
declare const SelectInputContext: React.Context<SelectInputProps>;
|
||||
export declare function useSelectInputContext(): SelectInputProps;
|
||||
export default SelectInputContext;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import * as React from 'react';
|
||||
const SelectInputContext = /*#__PURE__*/React.createContext(null);
|
||||
export function useSelectInputContext() {
|
||||
return React.useContext(SelectInputContext);
|
||||
}
|
||||
export default SelectInputContext;
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import * as React from 'react';
|
||||
import type { DisplayValueType, Mode, RenderNode } from '../interface';
|
||||
import type { ComponentsConfig } from '../hooks/useComponents';
|
||||
export interface SelectInputRef {
|
||||
focus: (options?: FocusOptions) => void;
|
||||
blur: () => void;
|
||||
nativeElement: HTMLDivElement;
|
||||
}
|
||||
export interface SelectInputProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'prefix'> {
|
||||
prefixCls: string;
|
||||
prefix?: React.ReactNode;
|
||||
suffix?: React.ReactNode;
|
||||
clearIcon?: React.ReactNode;
|
||||
removeIcon?: RenderNode;
|
||||
multiple?: boolean;
|
||||
displayValues: DisplayValueType[];
|
||||
placeholder?: React.ReactNode;
|
||||
searchValue?: string;
|
||||
activeValue?: string;
|
||||
mode?: Mode;
|
||||
autoClearSearchValue?: boolean;
|
||||
onSearch?: (searchText: string, fromTyping: boolean, isCompositing: boolean) => void;
|
||||
onSearchSubmit?: (searchText: string) => void;
|
||||
onInputBlur?: () => void;
|
||||
onClearMouseDown?: React.MouseEventHandler<HTMLElement>;
|
||||
onInputKeyDown?: React.KeyboardEventHandler<HTMLInputElement | HTMLTextAreaElement>;
|
||||
onSelectorRemove?: (value: DisplayValueType) => void;
|
||||
maxLength?: number;
|
||||
autoFocus?: boolean;
|
||||
/** Check if `tokenSeparators` contains `\n` or `\r\n` */
|
||||
tokenWithEnter?: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
focused?: boolean;
|
||||
components: ComponentsConfig;
|
||||
children?: React.ReactElement;
|
||||
}
|
||||
declare const _default: React.ForwardRefExoticComponent<SelectInputProps & React.RefAttributes<SelectInputRef>>;
|
||||
export default _default;
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
||||
import * as React from 'react';
|
||||
import Affix from "./Affix";
|
||||
import SelectContent from "./Content";
|
||||
import SelectInputContext from "./context";
|
||||
import useBaseProps from "../hooks/useBaseProps";
|
||||
import { omit, useEvent } from '@rc-component/util';
|
||||
import KeyCode from "@rc-component/util/es/KeyCode";
|
||||
import { isValidateOpenKey } from "../utils/keyUtil";
|
||||
import { clsx } from 'clsx';
|
||||
import { getDOM } from "@rc-component/util/es/Dom/findDOMNode";
|
||||
import { composeRef } from "@rc-component/util/es/ref";
|
||||
import pickAttrs from "@rc-component/util/es/pickAttrs";
|
||||
const DEFAULT_OMIT_PROPS = ['value', 'onChange', 'removeIcon', 'placeholder', 'maxTagCount', 'maxTagTextLength', 'maxTagPlaceholder', 'choiceTransitionName', 'onInputKeyDown', 'onPopupScroll', 'tabIndex', 'activeValue', 'onSelectorRemove', 'focused'];
|
||||
export default /*#__PURE__*/React.forwardRef(function SelectInput(props, ref) {
|
||||
const {
|
||||
// Style
|
||||
prefixCls,
|
||||
className,
|
||||
style,
|
||||
// UI
|
||||
prefix,
|
||||
suffix,
|
||||
clearIcon,
|
||||
children,
|
||||
// Data
|
||||
multiple,
|
||||
displayValues,
|
||||
placeholder,
|
||||
mode,
|
||||
// Search
|
||||
searchValue,
|
||||
onSearch,
|
||||
onSearchSubmit,
|
||||
onInputBlur,
|
||||
// Input
|
||||
maxLength,
|
||||
autoFocus,
|
||||
// Events
|
||||
onMouseDown,
|
||||
onClearMouseDown,
|
||||
onInputKeyDown,
|
||||
onSelectorRemove,
|
||||
// Token handling
|
||||
tokenWithEnter,
|
||||
// Components
|
||||
components,
|
||||
...restProps
|
||||
} = props;
|
||||
const {
|
||||
triggerOpen,
|
||||
toggleOpen,
|
||||
showSearch,
|
||||
disabled,
|
||||
loading,
|
||||
classNames,
|
||||
styles
|
||||
} = useBaseProps();
|
||||
const rootRef = React.useRef(null);
|
||||
const inputRef = React.useRef(null);
|
||||
|
||||
// Handle keyboard events similar to original Selector
|
||||
const onInternalInputKeyDown = useEvent(event => {
|
||||
const {
|
||||
which
|
||||
} = event;
|
||||
|
||||
// Compatible with multiple lines in TextArea
|
||||
const isTextAreaElement = inputRef.current instanceof HTMLTextAreaElement;
|
||||
|
||||
// Prevent default behavior for up/down arrows when dropdown is open
|
||||
if (!isTextAreaElement && triggerOpen && (which === KeyCode.UP || which === KeyCode.DOWN)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// Call the original onInputKeyDown callback
|
||||
if (onInputKeyDown) {
|
||||
onInputKeyDown(event);
|
||||
}
|
||||
|
||||
// Move within the text box for TextArea
|
||||
if (isTextAreaElement && !triggerOpen && ~[KeyCode.UP, KeyCode.DOWN, KeyCode.LEFT, KeyCode.RIGHT].indexOf(which)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Open dropdown when a valid open key is pressed
|
||||
const isModifier = event.ctrlKey || event.altKey || event.metaKey;
|
||||
if (!isModifier && isValidateOpenKey(which)) {
|
||||
toggleOpen(true);
|
||||
}
|
||||
});
|
||||
|
||||
// ====================== Refs ======================
|
||||
React.useImperativeHandle(ref, () => {
|
||||
return {
|
||||
focus: options => {
|
||||
// Focus the inner input if available, otherwise fall back to root div.
|
||||
(inputRef.current || rootRef.current).focus?.(options);
|
||||
},
|
||||
blur: () => {
|
||||
(inputRef.current || rootRef.current).blur?.();
|
||||
},
|
||||
// Use getDOM to handle nested nativeElement structure (e.g., when RootComponent is antd Input)
|
||||
nativeElement: getDOM(rootRef.current)
|
||||
};
|
||||
});
|
||||
|
||||
// ====================== Open ======================
|
||||
const onInternalMouseDown = useEvent(event => {
|
||||
if (!disabled) {
|
||||
const inputDOM = getDOM(inputRef.current);
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/56002
|
||||
// Tell `useSelectTriggerControl` to ignore this event
|
||||
// When icon is dynamic render, the parentNode will miss
|
||||
// so we need to mark the event directly
|
||||
event.nativeEvent._ori_target = inputDOM;
|
||||
const isClickOnInput = inputDOM === event.target || inputDOM?.contains(event.target);
|
||||
if (inputDOM && !isClickOnInput) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// Check if we should prevent closing when clicking on selector
|
||||
// Don't close if: open && not multiple && (combobox mode || showSearch)
|
||||
const shouldPreventCloseOnSingle = triggerOpen && !multiple && (mode === 'combobox' || showSearch);
|
||||
|
||||
// Don't close if: open && multiple && click on input
|
||||
const shouldPreventCloseOnMultipleInput = triggerOpen && multiple && isClickOnInput;
|
||||
const shouldPreventClose = shouldPreventCloseOnSingle || shouldPreventCloseOnMultipleInput;
|
||||
if (!event.nativeEvent._select_lazy) {
|
||||
inputRef.current?.focus();
|
||||
|
||||
// Only toggle open if we should not prevent close
|
||||
if (!shouldPreventClose) {
|
||||
toggleOpen();
|
||||
}
|
||||
} else if (triggerOpen) {
|
||||
// Lazy should also close when click clear icon
|
||||
toggleOpen(false);
|
||||
}
|
||||
}
|
||||
onMouseDown?.(event);
|
||||
});
|
||||
|
||||
// =================== Components ===================
|
||||
const {
|
||||
root: RootComponent
|
||||
} = components;
|
||||
|
||||
// ===================== Render =====================
|
||||
const domProps = omit(restProps, DEFAULT_OMIT_PROPS);
|
||||
const ariaProps = pickAttrs(domProps, {
|
||||
aria: true
|
||||
});
|
||||
const ariaKeys = Object.keys(ariaProps);
|
||||
|
||||
// Create context value with wrapped callbacks
|
||||
const contextValue = {
|
||||
...props,
|
||||
onInputKeyDown: onInternalInputKeyDown
|
||||
};
|
||||
if (RootComponent) {
|
||||
const originProps = RootComponent.props || {};
|
||||
const mergedProps = {
|
||||
...originProps,
|
||||
...domProps
|
||||
};
|
||||
Object.keys(originProps).forEach(key => {
|
||||
const originVal = originProps[key];
|
||||
const domVal = domProps[key];
|
||||
if (typeof originVal === 'function' && typeof domVal === 'function') {
|
||||
mergedProps[key] = (...args) => {
|
||||
domVal(...args);
|
||||
originVal(...args);
|
||||
};
|
||||
}
|
||||
});
|
||||
if ( /*#__PURE__*/React.isValidElement(RootComponent)) {
|
||||
return /*#__PURE__*/React.cloneElement(RootComponent, {
|
||||
...mergedProps,
|
||||
ref: composeRef(RootComponent.ref, rootRef)
|
||||
});
|
||||
}
|
||||
return /*#__PURE__*/React.createElement(RootComponent, _extends({}, mergedProps, {
|
||||
ref: rootRef
|
||||
}));
|
||||
}
|
||||
return /*#__PURE__*/React.createElement(SelectInputContext.Provider, {
|
||||
value: contextValue
|
||||
}, /*#__PURE__*/React.createElement("div", _extends({}, omit(domProps, ariaKeys), {
|
||||
// Style
|
||||
ref: rootRef,
|
||||
className: className,
|
||||
style: style
|
||||
// Mouse Events
|
||||
,
|
||||
onMouseDown: onInternalMouseDown
|
||||
}), /*#__PURE__*/React.createElement(Affix, {
|
||||
className: clsx(`${prefixCls}-prefix`, classNames?.prefix),
|
||||
style: styles?.prefix
|
||||
}, prefix), /*#__PURE__*/React.createElement(SelectContent, {
|
||||
ref: inputRef
|
||||
}), /*#__PURE__*/React.createElement(Affix, {
|
||||
className: clsx(`${prefixCls}-suffix`, {
|
||||
[`${prefixCls}-suffix-loading`]: loading
|
||||
}, classNames?.suffix),
|
||||
style: styles?.suffix
|
||||
}, suffix), clearIcon && /*#__PURE__*/React.createElement(Affix, {
|
||||
className: clsx(`${prefixCls}-clear`, classNames?.clear),
|
||||
style: styles?.clear,
|
||||
onMouseDown: e => {
|
||||
// Mark to tell not trigger open or focus
|
||||
e.nativeEvent._select_lazy = true;
|
||||
onClearMouseDown?.(e);
|
||||
}
|
||||
}, clearIcon), children));
|
||||
});
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import type { AlignType, BuildInPlacements } from '@rc-component/trigger/lib/interface';
|
||||
import * as React from 'react';
|
||||
import type { Placement, RenderDOMFunc } from './BaseSelect';
|
||||
export interface RefTriggerProps {
|
||||
getPopupElement: () => HTMLDivElement;
|
||||
}
|
||||
export interface SelectTriggerProps {
|
||||
prefixCls: string;
|
||||
children: React.ReactElement;
|
||||
disabled: boolean;
|
||||
visible: boolean;
|
||||
popupElement: React.ReactElement;
|
||||
animation?: string;
|
||||
transitionName?: string;
|
||||
placement?: Placement;
|
||||
builtinPlacements?: BuildInPlacements;
|
||||
popupStyle: React.CSSProperties;
|
||||
popupClassName: string;
|
||||
direction: string;
|
||||
popupMatchSelectWidth?: boolean | number;
|
||||
popupRender?: (menu: React.ReactElement) => React.ReactElement;
|
||||
getPopupContainer?: RenderDOMFunc;
|
||||
popupAlign: AlignType;
|
||||
empty: boolean;
|
||||
onPopupVisibleChange?: (visible: boolean) => void;
|
||||
onPopupMouseEnter: () => void;
|
||||
onPopupMouseDown: React.MouseEventHandler<HTMLDivElement>;
|
||||
onPopupBlur?: React.FocusEventHandler<HTMLDivElement>;
|
||||
}
|
||||
declare const RefSelectTrigger: React.ForwardRefExoticComponent<SelectTriggerProps & React.RefAttributes<RefTriggerProps>>;
|
||||
export default RefSelectTrigger;
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
||||
import Trigger from '@rc-component/trigger';
|
||||
import { clsx } from 'clsx';
|
||||
import * as React from 'react';
|
||||
const getBuiltInPlacements = popupMatchSelectWidth => {
|
||||
// Enable horizontal overflow auto-adjustment when a custom dropdown width is provided
|
||||
const adjustX = popupMatchSelectWidth === true ? 0 : 1;
|
||||
return {
|
||||
bottomLeft: {
|
||||
points: ['tl', 'bl'],
|
||||
offset: [0, 4],
|
||||
overflow: {
|
||||
adjustX,
|
||||
adjustY: 1
|
||||
},
|
||||
htmlRegion: 'scroll'
|
||||
},
|
||||
bottomRight: {
|
||||
points: ['tr', 'br'],
|
||||
offset: [0, 4],
|
||||
overflow: {
|
||||
adjustX,
|
||||
adjustY: 1
|
||||
},
|
||||
htmlRegion: 'scroll'
|
||||
},
|
||||
topLeft: {
|
||||
points: ['bl', 'tl'],
|
||||
offset: [0, -4],
|
||||
overflow: {
|
||||
adjustX,
|
||||
adjustY: 1
|
||||
},
|
||||
htmlRegion: 'scroll'
|
||||
},
|
||||
topRight: {
|
||||
points: ['br', 'tr'],
|
||||
offset: [0, -4],
|
||||
overflow: {
|
||||
adjustX,
|
||||
adjustY: 1
|
||||
},
|
||||
htmlRegion: 'scroll'
|
||||
}
|
||||
};
|
||||
};
|
||||
const SelectTrigger = (props, ref) => {
|
||||
const {
|
||||
prefixCls,
|
||||
disabled,
|
||||
visible,
|
||||
children,
|
||||
popupElement,
|
||||
animation,
|
||||
transitionName,
|
||||
popupStyle,
|
||||
popupClassName,
|
||||
direction = 'ltr',
|
||||
placement,
|
||||
builtinPlacements,
|
||||
popupMatchSelectWidth,
|
||||
popupRender,
|
||||
popupAlign,
|
||||
getPopupContainer,
|
||||
empty,
|
||||
onPopupVisibleChange,
|
||||
onPopupMouseEnter,
|
||||
onPopupMouseDown,
|
||||
onPopupBlur,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
// We still use `dropdown` className to keep compatibility
|
||||
// This is used for:
|
||||
// 1. Styles
|
||||
// 2. Animation
|
||||
// 3. Theme customization
|
||||
// Please do not modify this since it's a breaking change
|
||||
const popupPrefixCls = `${prefixCls}-dropdown`;
|
||||
let popupNode = popupElement;
|
||||
if (popupRender) {
|
||||
popupNode = popupRender(popupElement);
|
||||
}
|
||||
const mergedBuiltinPlacements = React.useMemo(() => builtinPlacements || getBuiltInPlacements(popupMatchSelectWidth), [builtinPlacements, popupMatchSelectWidth]);
|
||||
|
||||
// ===================== Motion ======================
|
||||
const mergedTransitionName = animation ? `${popupPrefixCls}-${animation}` : transitionName;
|
||||
|
||||
// =================== Popup Width ===================
|
||||
const isNumberPopupWidth = typeof popupMatchSelectWidth === 'number';
|
||||
const stretch = React.useMemo(() => {
|
||||
if (isNumberPopupWidth) {
|
||||
return null;
|
||||
}
|
||||
return popupMatchSelectWidth === false ? 'minWidth' : 'width';
|
||||
}, [popupMatchSelectWidth, isNumberPopupWidth]);
|
||||
let mergedPopupStyle = popupStyle;
|
||||
if (isNumberPopupWidth) {
|
||||
mergedPopupStyle = {
|
||||
...popupStyle,
|
||||
width: popupMatchSelectWidth
|
||||
};
|
||||
}
|
||||
|
||||
// ======================= Ref =======================
|
||||
const triggerPopupRef = React.useRef(null);
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
getPopupElement: () => triggerPopupRef.current?.popupElement
|
||||
}));
|
||||
return /*#__PURE__*/React.createElement(Trigger, _extends({}, restProps, {
|
||||
showAction: onPopupVisibleChange ? ['click'] : [],
|
||||
hideAction: onPopupVisibleChange ? ['click'] : [],
|
||||
popupPlacement: placement || (direction === 'rtl' ? 'bottomRight' : 'bottomLeft'),
|
||||
builtinPlacements: mergedBuiltinPlacements,
|
||||
prefixCls: popupPrefixCls,
|
||||
popupMotion: {
|
||||
motionName: mergedTransitionName
|
||||
},
|
||||
popup: /*#__PURE__*/React.createElement("div", {
|
||||
onMouseEnter: onPopupMouseEnter,
|
||||
onMouseDown: onPopupMouseDown,
|
||||
onBlur: onPopupBlur
|
||||
}, popupNode),
|
||||
ref: triggerPopupRef,
|
||||
stretch: stretch,
|
||||
popupAlign: popupAlign,
|
||||
popupVisible: visible,
|
||||
getPopupContainer: getPopupContainer,
|
||||
popupClassName: clsx(popupClassName, {
|
||||
[`${popupPrefixCls}-empty`]: empty
|
||||
}),
|
||||
popupStyle: mergedPopupStyle,
|
||||
onPopupVisibleChange: onPopupVisibleChange
|
||||
}), children);
|
||||
};
|
||||
const RefSelectTrigger = /*#__PURE__*/React.forwardRef(SelectTrigger);
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
RefSelectTrigger.displayName = 'SelectTrigger';
|
||||
}
|
||||
export default RefSelectTrigger;
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import * as React from 'react';
|
||||
import type { RenderNode } from './BaseSelect';
|
||||
export interface TransBtnProps {
|
||||
className: string;
|
||||
style?: React.CSSProperties;
|
||||
customizeIcon: RenderNode;
|
||||
customizeIconProps?: any;
|
||||
onMouseDown?: React.MouseEventHandler<HTMLSpanElement>;
|
||||
onClick?: React.MouseEventHandler<HTMLSpanElement>;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
/**
|
||||
* Small wrapper for Select icons (clear/arrow/etc.).
|
||||
* Prevents default mousedown to avoid blurring or caret moves, and
|
||||
* renders a custom icon or a fallback icon span.
|
||||
*
|
||||
* DOM structure:
|
||||
* <span className={className} ...>
|
||||
* { icon || <span className={`${className}-icon`}>{children}</span> }
|
||||
* </span>
|
||||
*/
|
||||
declare const TransBtn: React.FC<TransBtnProps>;
|
||||
export default TransBtn;
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import * as React from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
/**
|
||||
* Small wrapper for Select icons (clear/arrow/etc.).
|
||||
* Prevents default mousedown to avoid blurring or caret moves, and
|
||||
* renders a custom icon or a fallback icon span.
|
||||
*
|
||||
* DOM structure:
|
||||
* <span className={className} ...>
|
||||
* { icon || <span className={`${className}-icon`}>{children}</span> }
|
||||
* </span>
|
||||
*/
|
||||
const TransBtn = props => {
|
||||
const {
|
||||
className,
|
||||
style,
|
||||
customizeIcon,
|
||||
customizeIconProps,
|
||||
children,
|
||||
onMouseDown,
|
||||
onClick
|
||||
} = props;
|
||||
const icon = typeof customizeIcon === 'function' ? customizeIcon(customizeIconProps) : customizeIcon;
|
||||
return /*#__PURE__*/React.createElement("span", {
|
||||
className: className,
|
||||
onMouseDown: event => {
|
||||
event.preventDefault();
|
||||
onMouseDown?.(event);
|
||||
},
|
||||
style: {
|
||||
userSelect: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
...style
|
||||
},
|
||||
unselectable: "on",
|
||||
onClick: onClick,
|
||||
"aria-hidden": true
|
||||
}, icon !== undefined ? icon : /*#__PURE__*/React.createElement("span", {
|
||||
className: clsx(className.split(/\s+/).map(cls => `${cls}-icon`))
|
||||
}, children));
|
||||
};
|
||||
export default TransBtn;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import type { DisplayValueType, Mode } from '../interface';
|
||||
import type React from 'react';
|
||||
export interface AllowClearConfig {
|
||||
allowClear: boolean;
|
||||
clearIcon: React.ReactNode;
|
||||
}
|
||||
export declare const useAllowClear: (prefixCls: string, displayValues: DisplayValueType[], allowClear?: boolean | {
|
||||
clearIcon?: React.ReactNode;
|
||||
}, clearIcon?: React.ReactNode, disabled?: boolean, mergedSearchValue?: string, mode?: Mode) => AllowClearConfig;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { useMemo } from 'react';
|
||||
export const useAllowClear = (prefixCls, displayValues, allowClear, clearIcon, disabled = false, mergedSearchValue, mode) => {
|
||||
// Convert boolean to object first
|
||||
const allowClearConfig = useMemo(() => {
|
||||
if (typeof allowClear === 'boolean') {
|
||||
return {
|
||||
allowClear
|
||||
};
|
||||
}
|
||||
if (allowClear && typeof allowClear === 'object') {
|
||||
return allowClear;
|
||||
}
|
||||
return {
|
||||
allowClear: false
|
||||
};
|
||||
}, [allowClear]);
|
||||
return useMemo(() => {
|
||||
const mergedAllowClear = !disabled && allowClearConfig.allowClear !== false && (displayValues.length || mergedSearchValue) && !(mode === 'combobox' && mergedSearchValue === '');
|
||||
return {
|
||||
allowClear: mergedAllowClear,
|
||||
clearIcon: mergedAllowClear ? allowClearConfig.clearIcon || clearIcon || '×' : null
|
||||
};
|
||||
}, [allowClearConfig, clearIcon, disabled, displayValues.length, mergedSearchValue, mode]);
|
||||
};
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* BaseSelect provide some parsed data into context.
|
||||
* You can use this hooks to get them.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import type { BaseSelectProps } from '../BaseSelect';
|
||||
export interface BaseSelectContextProps extends BaseSelectProps {
|
||||
triggerOpen: boolean;
|
||||
rawOpen: boolean;
|
||||
multiple: boolean;
|
||||
toggleOpen: (open?: boolean) => void;
|
||||
lockOptions: boolean;
|
||||
}
|
||||
export declare const BaseSelectContext: React.Context<BaseSelectContextProps>;
|
||||
export default function useBaseProps(): BaseSelectContextProps;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* BaseSelect provide some parsed data into context.
|
||||
* You can use this hooks to get them.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
export const BaseSelectContext = /*#__PURE__*/React.createContext(null);
|
||||
export default function useBaseProps() {
|
||||
return React.useContext(BaseSelectContext);
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import type { RawValueType } from '../BaseSelect';
|
||||
import type { DefaultOptionType, LabelInValueType } from '../Select';
|
||||
/**
|
||||
* Cache `value` related LabeledValue & options.
|
||||
*/
|
||||
declare const _default: (labeledValues: LabelInValueType[], valueOptions: Map<RawValueType, DefaultOptionType>) => [LabelInValueType[], (val: RawValueType) => DefaultOptionType];
|
||||
export default _default;
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import * as React from 'react';
|
||||
/**
|
||||
* Cache `value` related LabeledValue & options.
|
||||
*/
|
||||
export default ((labeledValues, valueOptions) => {
|
||||
const cacheRef = React.useRef({
|
||||
values: new Map(),
|
||||
options: new Map()
|
||||
});
|
||||
const filledLabeledValues = React.useMemo(() => {
|
||||
const {
|
||||
values: prevValueCache,
|
||||
options: prevOptionCache
|
||||
} = cacheRef.current;
|
||||
|
||||
// Fill label by cache
|
||||
const patchedValues = labeledValues.map(item => {
|
||||
if (item.label === undefined) {
|
||||
return {
|
||||
...item,
|
||||
label: prevValueCache.get(item.value)?.label
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
// Refresh cache
|
||||
const valueCache = new Map();
|
||||
const optionCache = new Map();
|
||||
patchedValues.forEach(item => {
|
||||
valueCache.set(item.value, item);
|
||||
optionCache.set(item.value, valueOptions.get(item.value) || prevOptionCache.get(item.value));
|
||||
});
|
||||
cacheRef.current.values = valueCache;
|
||||
cacheRef.current.options = optionCache;
|
||||
return patchedValues;
|
||||
}, [labeledValues, valueOptions]);
|
||||
const getOption = React.useCallback(val => valueOptions.get(val) || cacheRef.current.options.get(val), [valueOptions]);
|
||||
return [filledLabeledValues, getOption];
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import type { SelectInputRef, SelectInputProps } from '../SelectInput';
|
||||
import type { BaseSelectProps } from '../BaseSelect';
|
||||
export interface ComponentsConfig {
|
||||
root?: React.ComponentType<any> | string | React.ReactElement;
|
||||
input?: React.ComponentType<any> | string | React.ReactElement;
|
||||
}
|
||||
export interface FilledComponentsConfig {
|
||||
root: React.ForwardRefExoticComponent<SelectInputProps & React.RefAttributes<SelectInputRef>>;
|
||||
input: React.ForwardRefExoticComponent<React.TextareaHTMLAttributes<HTMLTextAreaElement> | (React.InputHTMLAttributes<HTMLInputElement> & React.RefAttributes<HTMLInputElement | HTMLTextAreaElement>)>;
|
||||
}
|
||||
export default function useComponents(components?: ComponentsConfig, getInputElement?: BaseSelectProps['getInputElement'], getRawInputElement?: BaseSelectProps['getRawInputElement']): ComponentsConfig;
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import * as React from 'react';
|
||||
export default function useComponents(components, getInputElement, getRawInputElement) {
|
||||
return React.useMemo(() => {
|
||||
let {
|
||||
root,
|
||||
input
|
||||
} = components || {};
|
||||
|
||||
// root: getRawInputElement
|
||||
if (getRawInputElement) {
|
||||
root = getRawInputElement();
|
||||
}
|
||||
|
||||
// input: getInputElement
|
||||
if (getInputElement) {
|
||||
input = getInputElement();
|
||||
}
|
||||
return {
|
||||
root,
|
||||
input
|
||||
};
|
||||
}, [components, getInputElement, getRawInputElement]);
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import type { FieldNames, DefaultOptionType, SelectProps } from '../Select';
|
||||
declare const _default: (options: DefaultOptionType[], fieldNames: FieldNames, searchValue?: string, filterOption?: SelectProps['filterOption'], optionFilterProp?: string[]) => DefaultOptionType[];
|
||||
export default _default;
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import * as React from 'react';
|
||||
import { toArray } from "../utils/commonUtil";
|
||||
import { injectPropsWithOption } from "../utils/valueUtil";
|
||||
function includes(test, search) {
|
||||
return toArray(test).join('').toUpperCase().includes(search);
|
||||
}
|
||||
export default ((options, fieldNames, searchValue, filterOption, optionFilterProp) => {
|
||||
return React.useMemo(() => {
|
||||
if (!searchValue || filterOption === false) {
|
||||
return options;
|
||||
}
|
||||
const {
|
||||
options: fieldOptions,
|
||||
label: fieldLabel,
|
||||
value: fieldValue
|
||||
} = fieldNames;
|
||||
const filteredOptions = [];
|
||||
const customizeFilter = typeof filterOption === 'function';
|
||||
const upperSearch = searchValue.toUpperCase();
|
||||
const filterFunc = customizeFilter ? filterOption : (_, option) => {
|
||||
// Use provided `optionFilterProp`
|
||||
if (optionFilterProp && optionFilterProp.length) {
|
||||
return optionFilterProp.some(prop => includes(option[prop], upperSearch));
|
||||
}
|
||||
|
||||
// Auto select `label` or `value` by option type
|
||||
if (option[fieldOptions]) {
|
||||
// hack `fieldLabel` since `OptionGroup` children is not `label`
|
||||
return includes(option[fieldLabel !== 'children' ? fieldLabel : 'label'], upperSearch);
|
||||
}
|
||||
return includes(option[fieldValue], upperSearch);
|
||||
};
|
||||
const wrapOption = customizeFilter ? opt => injectPropsWithOption(opt) : opt => opt;
|
||||
options.forEach(item => {
|
||||
// Group should check child options
|
||||
if (item[fieldOptions]) {
|
||||
// Check group first
|
||||
const matchGroup = filterFunc(searchValue, wrapOption(item));
|
||||
if (matchGroup) {
|
||||
filteredOptions.push(item);
|
||||
} else {
|
||||
// Check option
|
||||
const subOptions = item[fieldOptions].filter(subItem => filterFunc(searchValue, wrapOption(subItem)));
|
||||
if (subOptions.length) {
|
||||
filteredOptions.push({
|
||||
...item,
|
||||
[fieldOptions]: subOptions
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (filterFunc(searchValue, wrapOption(item))) {
|
||||
filteredOptions.push(item);
|
||||
}
|
||||
});
|
||||
return filteredOptions;
|
||||
}, [options, filterOption, optionFilterProp, searchValue, fieldNames]);
|
||||
});
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Locker return cached mark.
|
||||
* If set to `true`, will return `true` in a short time even if set `false`.
|
||||
* If set to `false` and then set to `true`, will change to `true`.
|
||||
* And after time duration, it will back to `null` automatically.
|
||||
*/
|
||||
export default function useLock(duration?: number): [() => boolean, (lock: boolean) => void];
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* Locker return cached mark.
|
||||
* If set to `true`, will return `true` in a short time even if set `false`.
|
||||
* If set to `false` and then set to `true`, will change to `true`.
|
||||
* And after time duration, it will back to `null` automatically.
|
||||
*/
|
||||
export default function useLock(duration = 250) {
|
||||
const lockRef = React.useRef(null);
|
||||
const timeoutRef = React.useRef(null);
|
||||
|
||||
// Clean up
|
||||
React.useEffect(() => () => {
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
}, []);
|
||||
function doLock(locked) {
|
||||
if (locked || lockRef.current === null) {
|
||||
lockRef.current = locked;
|
||||
}
|
||||
window.clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
lockRef.current = null;
|
||||
}, duration);
|
||||
}
|
||||
return [() => lockRef.current, doLock];
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
export declare const macroTask: (fn: VoidFunction, times?: number) => void;
|
||||
/**
|
||||
* Trigger by latest open call, if nextOpen is undefined, means toggle.
|
||||
* `weak` means this call can be ignored if previous call exists.
|
||||
*/
|
||||
export type TriggerOpenType = (nextOpen?: boolean, config?: {
|
||||
cancelFun?: () => boolean;
|
||||
}) => void;
|
||||
/**
|
||||
* When `open` is controlled, follow the controlled value;
|
||||
* Otherwise use uncontrolled logic.
|
||||
* Setting `open` takes effect immediately,
|
||||
* but setting it to `false` is delayed via MessageChannel.
|
||||
*
|
||||
* SSR handling: During SSR, `open` is always false to avoid Portal issues.
|
||||
* On client-side hydration, it syncs with the actual open state.
|
||||
*/
|
||||
export default function useOpen(defaultOpen: boolean, propOpen: boolean, onOpen: (nextOpen: boolean) => void, postOpen: (nextOpen: boolean) => boolean): [rawOpen: boolean, open: boolean, toggleOpen: TriggerOpenType, lockOptions: boolean];
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import { useControlledState, useEvent } from '@rc-component/util';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
const internalMacroTask = fn => {
|
||||
const channel = new MessageChannel();
|
||||
channel.port1.onmessage = fn;
|
||||
channel.port2.postMessage(null);
|
||||
};
|
||||
export const macroTask = (fn, times = 1) => {
|
||||
if (times <= 0) {
|
||||
fn();
|
||||
return;
|
||||
}
|
||||
internalMacroTask(() => {
|
||||
macroTask(fn, times - 1);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Trigger by latest open call, if nextOpen is undefined, means toggle.
|
||||
* `weak` means this call can be ignored if previous call exists.
|
||||
*/
|
||||
|
||||
/**
|
||||
* When `open` is controlled, follow the controlled value;
|
||||
* Otherwise use uncontrolled logic.
|
||||
* Setting `open` takes effect immediately,
|
||||
* but setting it to `false` is delayed via MessageChannel.
|
||||
*
|
||||
* SSR handling: During SSR, `open` is always false to avoid Portal issues.
|
||||
* On client-side hydration, it syncs with the actual open state.
|
||||
*/
|
||||
export default function useOpen(defaultOpen, propOpen, onOpen, postOpen) {
|
||||
// SSR not support Portal which means we need delay `open` for the first time render
|
||||
const [rendered, setRendered] = useState(false);
|
||||
useEffect(() => {
|
||||
setRendered(true);
|
||||
}, []);
|
||||
const [stateOpen, internalSetOpen] = useControlledState(defaultOpen, propOpen);
|
||||
|
||||
// Lock for options update
|
||||
const [lock, setLock] = useState(false);
|
||||
|
||||
// During SSR, always return false for open state
|
||||
const ssrSafeOpen = rendered ? stateOpen : false;
|
||||
const mergedOpen = postOpen(ssrSafeOpen);
|
||||
const taskIdRef = useRef(0);
|
||||
const triggerEvent = useEvent(nextOpen => {
|
||||
if (onOpen && mergedOpen !== nextOpen) {
|
||||
onOpen(nextOpen);
|
||||
}
|
||||
internalSetOpen(nextOpen);
|
||||
});
|
||||
const toggleOpen = useEvent((nextOpen, config = {}) => {
|
||||
const {
|
||||
cancelFun
|
||||
} = config;
|
||||
taskIdRef.current += 1;
|
||||
const id = taskIdRef.current;
|
||||
const nextOpenVal = typeof nextOpen === 'boolean' ? nextOpen : !mergedOpen;
|
||||
setLock(!nextOpenVal);
|
||||
function triggerUpdate() {
|
||||
if (
|
||||
// Always check if id is match
|
||||
id === taskIdRef.current &&
|
||||
// Check if need to cancel
|
||||
!cancelFun?.()) {
|
||||
triggerEvent(nextOpenVal);
|
||||
setLock(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Weak update can be ignored
|
||||
if (nextOpenVal) {
|
||||
triggerUpdate();
|
||||
} else {
|
||||
macroTask(() => {
|
||||
triggerUpdate();
|
||||
});
|
||||
}
|
||||
});
|
||||
return [ssrSafeOpen, mergedOpen, toggleOpen, lock];
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import type { FieldNames, RawValueType } from '../Select';
|
||||
/**
|
||||
* Parse `children` to `options` if `options` is not provided.
|
||||
* Then flatten the `options`.
|
||||
*/
|
||||
declare const useOptions: <OptionType>(options: OptionType[], children: React.ReactNode, fieldNames: FieldNames, optionFilterProp: string[], optionLabelProp: string) => {
|
||||
options: OptionType[];
|
||||
valueOptions: Map<RawValueType, OptionType>;
|
||||
labelOptions: Map<React.ReactNode, OptionType>;
|
||||
};
|
||||
export default useOptions;
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import * as React from 'react';
|
||||
import { convertChildrenToData } from "../utils/legacyUtil";
|
||||
|
||||
/**
|
||||
* Parse `children` to `options` if `options` is not provided.
|
||||
* Then flatten the `options`.
|
||||
*/
|
||||
const useOptions = (options, children, fieldNames, optionFilterProp, optionLabelProp) => {
|
||||
return React.useMemo(() => {
|
||||
let mergedOptions = options;
|
||||
const childrenAsData = !options;
|
||||
if (childrenAsData) {
|
||||
mergedOptions = convertChildrenToData(children);
|
||||
}
|
||||
const valueOptions = new Map();
|
||||
const labelOptions = new Map();
|
||||
const setLabelOptions = (labelOptionsMap, option, key) => {
|
||||
if (key && typeof key === 'string') {
|
||||
labelOptionsMap.set(option[key], option);
|
||||
}
|
||||
};
|
||||
const dig = (optionList, isChildren = false) => {
|
||||
// for loop to speed up collection speed
|
||||
for (let i = 0; i < optionList.length; i += 1) {
|
||||
const option = optionList[i];
|
||||
if (!option[fieldNames.options] || isChildren) {
|
||||
valueOptions.set(option[fieldNames.value], option);
|
||||
setLabelOptions(labelOptions, option, fieldNames.label);
|
||||
// https://github.com/ant-design/ant-design/issues/35304
|
||||
optionFilterProp.forEach(prop => {
|
||||
setLabelOptions(labelOptions, option, prop);
|
||||
});
|
||||
setLabelOptions(labelOptions, option, optionLabelProp);
|
||||
} else {
|
||||
dig(option[fieldNames.options], true);
|
||||
}
|
||||
}
|
||||
};
|
||||
dig(mergedOptions);
|
||||
return {
|
||||
options: mergedOptions,
|
||||
valueOptions,
|
||||
labelOptions
|
||||
};
|
||||
}, [options, children, fieldNames, optionFilterProp, optionLabelProp]);
|
||||
};
|
||||
export default useOptions;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Same as `React.useCallback` but always return a memoized function
|
||||
* but redirect to real function.
|
||||
*/
|
||||
export default function useRefFunc<T extends (...args: any[]) => any>(callback: T): T;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* Same as `React.useCallback` but always return a memoized function
|
||||
* but redirect to real function.
|
||||
*/
|
||||
export default function useRefFunc(callback) {
|
||||
const funcRef = React.useRef();
|
||||
funcRef.current = callback;
|
||||
const cacheFn = React.useCallback((...args) => {
|
||||
return funcRef.current(...args);
|
||||
}, []);
|
||||
return cacheFn;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
import type { SearchConfig, DefaultOptionType, SelectProps } from '../Select';
|
||||
export default function useSearchConfig(showSearch: boolean | SearchConfig<DefaultOptionType> | undefined, props: SearchConfig<DefaultOptionType>, mode: SelectProps<DefaultOptionType>['mode']): [boolean, SearchConfig<DefaultOptionType>];
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react';
|
||||
|
||||
// Convert `showSearch` to unique config
|
||||
export default function useSearchConfig(showSearch, props, mode) {
|
||||
const {
|
||||
filterOption,
|
||||
searchValue,
|
||||
optionFilterProp,
|
||||
filterSort,
|
||||
onSearch,
|
||||
autoClearSearchValue
|
||||
} = props;
|
||||
return React.useMemo(() => {
|
||||
const isObject = typeof showSearch === 'object';
|
||||
const searchConfig = {
|
||||
filterOption,
|
||||
searchValue,
|
||||
optionFilterProp,
|
||||
filterSort,
|
||||
onSearch,
|
||||
autoClearSearchValue,
|
||||
...(isObject ? showSearch : {})
|
||||
};
|
||||
return [isObject || mode === 'combobox' || mode === 'tags' || mode === 'multiple' && showSearch === undefined ? true : showSearch, searchConfig];
|
||||
}, [mode, showSearch, filterOption, searchValue, optionFilterProp, filterSort, onSearch, autoClearSearchValue]);
|
||||
}
|
||||
Generated
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
import type { TriggerOpenType } from './useOpen';
|
||||
export declare function isInside(elements: (HTMLElement | SVGElement | undefined)[], target: HTMLElement): boolean;
|
||||
export default function useSelectTriggerControl(elements: () => (HTMLElement | SVGElement | undefined)[], open: boolean, triggerOpen: TriggerOpenType, customizedTrigger: boolean): void;
|
||||
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
import { useEvent } from '@rc-component/util';
|
||||
export function isInside(elements, target) {
|
||||
return elements.filter(element => element).some(element => element.contains(target) || element === target);
|
||||
}
|
||||
export default function useSelectTriggerControl(elements, open, triggerOpen, customizedTrigger) {
|
||||
const onGlobalMouseDown = useEvent(event => {
|
||||
// If trigger is customized, Trigger will take control of popupVisible
|
||||
if (customizedTrigger) {
|
||||
return;
|
||||
}
|
||||
let target = event.target;
|
||||
if (target.shadowRoot && event.composed) {
|
||||
target = event.composedPath()[0] || target;
|
||||
}
|
||||
if (event._ori_target) {
|
||||
target = event._ori_target;
|
||||
}
|
||||
if (open &&
|
||||
// Marked by SelectInput mouseDown event
|
||||
!isInside(elements(), target)) {
|
||||
// Should trigger close
|
||||
triggerOpen(false);
|
||||
}
|
||||
});
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('mousedown', onGlobalMouseDown);
|
||||
return () => window.removeEventListener('mousedown', onGlobalMouseDown);
|
||||
}, [onGlobalMouseDown]);
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import Select from './Select';
|
||||
import Option from './Option';
|
||||
import OptGroup from './OptGroup';
|
||||
import type { SelectProps } from './Select';
|
||||
import BaseSelect from './BaseSelect';
|
||||
import type { BaseSelectProps, BaseSelectRef, BaseSelectPropsWithoutPrivate } from './BaseSelect';
|
||||
import useBaseProps from './hooks/useBaseProps';
|
||||
export { Option, OptGroup, BaseSelect, useBaseProps };
|
||||
export type { SelectProps, BaseSelectProps, BaseSelectRef, BaseSelectPropsWithoutPrivate };
|
||||
export default Select;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import Select from "./Select";
|
||||
import Option from "./Option";
|
||||
import OptGroup from "./OptGroup";
|
||||
import BaseSelect from "./BaseSelect";
|
||||
import useBaseProps from "./hooks/useBaseProps";
|
||||
export { Option, OptGroup, BaseSelect, useBaseProps };
|
||||
export default Select;
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import type * as React from 'react';
|
||||
export type RawValueType = string | number;
|
||||
export interface FlattenOptionData<OptionType> {
|
||||
label?: React.ReactNode;
|
||||
data: OptionType;
|
||||
key: React.Key;
|
||||
value?: RawValueType;
|
||||
groupOption?: boolean;
|
||||
group?: boolean;
|
||||
}
|
||||
export interface DisplayValueType {
|
||||
key?: React.Key;
|
||||
value?: RawValueType;
|
||||
label?: React.ReactNode;
|
||||
title?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
index?: number;
|
||||
}
|
||||
export type RenderNode = React.ReactNode | ((props: any) => React.ReactNode);
|
||||
export type RenderDOMFunc = (props: any) => HTMLElement;
|
||||
export type Mode = 'multiple' | 'tags' | 'combobox';
|
||||
export type Placement = 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topRight';
|
||||
export type DisplayInfoType = 'add' | 'remove' | 'clear';
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare function isPlatformMac(): boolean;
|
||||
Generated
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
export function isPlatformMac() {
|
||||
return true;
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import type { DisplayValueType } from '../BaseSelect';
|
||||
export declare function toArray<T>(value: T | T[]): T[];
|
||||
export declare const isClient: HTMLElement;
|
||||
/** Is client side and not jsdom */
|
||||
export declare const isBrowserClient: HTMLElement;
|
||||
export declare function hasValue(value: any): boolean;
|
||||
/** combo mode no value judgment function */
|
||||
export declare function isComboNoValue(value: any): boolean;
|
||||
export declare function getTitle(item: DisplayValueType): string;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
export function toArray(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
return value !== undefined ? [value] : [];
|
||||
}
|
||||
export const isClient = typeof window !== 'undefined' && window.document && window.document.documentElement;
|
||||
|
||||
/** Is client side and not jsdom */
|
||||
export const isBrowserClient = process.env.NODE_ENV !== 'test' && isClient;
|
||||
export function hasValue(value) {
|
||||
return value !== undefined && value !== null;
|
||||
}
|
||||
|
||||
/** combo mode no value judgment function */
|
||||
export function isComboNoValue(value) {
|
||||
return !value && value !== 0;
|
||||
}
|
||||
function isTitleType(title) {
|
||||
return ['string', 'number'].includes(typeof title);
|
||||
}
|
||||
export function getTitle(item) {
|
||||
let title = undefined;
|
||||
if (item) {
|
||||
if (isTitleType(item.title)) {
|
||||
title = item.title.toString();
|
||||
} else if (isTitleType(item.label)) {
|
||||
title = item.label.toString();
|
||||
}
|
||||
}
|
||||
return title;
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
/** keyCode Judgment function */
|
||||
export declare function isValidateOpenKey(currentKeyCode: number): boolean;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import KeyCode from "@rc-component/util/es/KeyCode";
|
||||
|
||||
/** keyCode Judgment function */
|
||||
export function isValidateOpenKey(currentKeyCode) {
|
||||
return (
|
||||
// Undefined for Edge bug:
|
||||
// https://github.com/ant-design/ant-design/issues/51292
|
||||
currentKeyCode &&
|
||||
// Other keys
|
||||
![
|
||||
// System function button
|
||||
KeyCode.ESC, KeyCode.SHIFT, KeyCode.BACKSPACE, KeyCode.TAB, KeyCode.WIN_KEY, KeyCode.ALT, KeyCode.META, KeyCode.WIN_KEY_RIGHT, KeyCode.CTRL, KeyCode.SEMICOLON, KeyCode.EQUALS, KeyCode.CAPS_LOCK, KeyCode.CONTEXT_MENU,
|
||||
// Arrow keys - should not trigger open when navigating in input
|
||||
KeyCode.UP,
|
||||
// KeyCode.DOWN,
|
||||
KeyCode.LEFT, KeyCode.RIGHT,
|
||||
// F1-F12
|
||||
KeyCode.F1, KeyCode.F2, KeyCode.F3, KeyCode.F4, KeyCode.F5, KeyCode.F6, KeyCode.F7, KeyCode.F8, KeyCode.F9, KeyCode.F10, KeyCode.F11, KeyCode.F12].includes(currentKeyCode)
|
||||
);
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import * as React from 'react';
|
||||
import type { BaseOptionType, DefaultOptionType } from '../Select';
|
||||
export declare function convertChildrenToData<OptionType extends BaseOptionType = DefaultOptionType>(nodes: React.ReactNode, optionOnly?: boolean): OptionType[];
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import * as React from 'react';
|
||||
import toArray from "@rc-component/util/es/Children/toArray";
|
||||
function convertNodeToOption(node) {
|
||||
const {
|
||||
key,
|
||||
props: {
|
||||
children,
|
||||
value,
|
||||
...restProps
|
||||
}
|
||||
} = node;
|
||||
return {
|
||||
key,
|
||||
value: value !== undefined ? value : key,
|
||||
children,
|
||||
...restProps
|
||||
};
|
||||
}
|
||||
export function convertChildrenToData(nodes, optionOnly = false) {
|
||||
return toArray(nodes).map((node, index) => {
|
||||
if (! /*#__PURE__*/React.isValidElement(node) || !node.type) {
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
type: {
|
||||
isSelectOptGroup
|
||||
},
|
||||
key,
|
||||
props: {
|
||||
children,
|
||||
...restProps
|
||||
}
|
||||
} = node;
|
||||
if (optionOnly || !isSelectOptGroup) {
|
||||
return convertNodeToOption(node);
|
||||
}
|
||||
return {
|
||||
key: `__RC_SELECT_GRP__${key === null ? index : key}__`,
|
||||
label: key,
|
||||
...restProps,
|
||||
options: convertChildrenToData(children)
|
||||
};
|
||||
}).filter(data => data);
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function isPlatformMac(): boolean;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
/* istanbul ignore file */
|
||||
export function isPlatformMac() {
|
||||
return /(mac\sos|macintosh)/i.test(navigator.appVersion);
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import type { BaseOptionType, DefaultOptionType } from '../Select';
|
||||
import type { FieldNames } from '../Select';
|
||||
import type { FlattenOptionData } from '../interface';
|
||||
export declare function isValidCount(value?: number): boolean;
|
||||
export declare function fillFieldNames(fieldNames: FieldNames | undefined, childrenAsData: boolean): {
|
||||
label: string;
|
||||
value: string;
|
||||
options: string;
|
||||
groupLabel: string;
|
||||
};
|
||||
/**
|
||||
* Flat options into flatten list.
|
||||
* We use `optionOnly` here is aim to avoid user use nested option group.
|
||||
* Here is simply set `key` to the index if not provided.
|
||||
*/
|
||||
export declare function flattenOptions<OptionType extends BaseOptionType = DefaultOptionType>(options: OptionType[], { fieldNames, childrenAsData }?: {
|
||||
fieldNames?: FieldNames;
|
||||
childrenAsData?: boolean;
|
||||
}): FlattenOptionData<OptionType>[];
|
||||
/**
|
||||
* Inject `props` into `option` for legacy usage
|
||||
*/
|
||||
export declare function injectPropsWithOption<T extends object>(option: T): T;
|
||||
export declare const getSeparatedContent: (text: string, tokens: string[], end?: number) => string[];
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import warning from "@rc-component/util/es/warning";
|
||||
function getKey(data, index) {
|
||||
const {
|
||||
key
|
||||
} = data;
|
||||
let value;
|
||||
if ('value' in data) {
|
||||
({
|
||||
value
|
||||
} = data);
|
||||
}
|
||||
if (key !== null && key !== undefined) {
|
||||
return key;
|
||||
}
|
||||
if (value !== undefined) {
|
||||
return value;
|
||||
}
|
||||
return `rc-index-key-${index}`;
|
||||
}
|
||||
export function isValidCount(value) {
|
||||
return typeof value !== 'undefined' && !Number.isNaN(value);
|
||||
}
|
||||
export function fillFieldNames(fieldNames, childrenAsData) {
|
||||
const {
|
||||
label,
|
||||
value,
|
||||
options,
|
||||
groupLabel
|
||||
} = fieldNames || {};
|
||||
const mergedLabel = label || (childrenAsData ? 'children' : 'label');
|
||||
return {
|
||||
label: mergedLabel,
|
||||
value: value || 'value',
|
||||
options: options || 'options',
|
||||
groupLabel: groupLabel || mergedLabel
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Flat options into flatten list.
|
||||
* We use `optionOnly` here is aim to avoid user use nested option group.
|
||||
* Here is simply set `key` to the index if not provided.
|
||||
*/
|
||||
export function flattenOptions(options, {
|
||||
fieldNames,
|
||||
childrenAsData
|
||||
} = {}) {
|
||||
const flattenList = [];
|
||||
const {
|
||||
label: fieldLabel,
|
||||
value: fieldValue,
|
||||
options: fieldOptions,
|
||||
groupLabel
|
||||
} = fillFieldNames(fieldNames, false);
|
||||
function dig(list, isGroupOption) {
|
||||
if (!Array.isArray(list)) {
|
||||
return;
|
||||
}
|
||||
list.forEach(data => {
|
||||
if (isGroupOption || !(fieldOptions in data)) {
|
||||
const value = data[fieldValue];
|
||||
|
||||
// Option
|
||||
flattenList.push({
|
||||
key: getKey(data, flattenList.length),
|
||||
groupOption: isGroupOption,
|
||||
data,
|
||||
label: data[fieldLabel],
|
||||
value
|
||||
});
|
||||
} else {
|
||||
let grpLabel = data[groupLabel];
|
||||
if (grpLabel === undefined && childrenAsData) {
|
||||
grpLabel = data.label;
|
||||
}
|
||||
|
||||
// Option Group
|
||||
flattenList.push({
|
||||
key: getKey(data, flattenList.length),
|
||||
group: true,
|
||||
data,
|
||||
label: grpLabel
|
||||
});
|
||||
dig(data[fieldOptions], true);
|
||||
}
|
||||
});
|
||||
}
|
||||
dig(options, false);
|
||||
return flattenList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject `props` into `option` for legacy usage
|
||||
*/
|
||||
export function injectPropsWithOption(option) {
|
||||
const newOption = {
|
||||
...option
|
||||
};
|
||||
if (!('props' in newOption)) {
|
||||
Object.defineProperty(newOption, 'props', {
|
||||
get() {
|
||||
warning(false, 'Return type is option instead of Option instance. Please read value directly instead of reading from `props`.');
|
||||
return newOption;
|
||||
}
|
||||
});
|
||||
}
|
||||
return newOption;
|
||||
}
|
||||
export const getSeparatedContent = (text, tokens, end) => {
|
||||
if (!tokens || !tokens.length) {
|
||||
return null;
|
||||
}
|
||||
let match = false;
|
||||
const separate = (str, [token, ...restTokens]) => {
|
||||
if (!token) {
|
||||
return [str];
|
||||
}
|
||||
const list = str.split(token);
|
||||
match = match || list.length > 1;
|
||||
return list.reduce((prevList, unitStr) => [...prevList, ...separate(unitStr, restTokens)], []).filter(Boolean);
|
||||
};
|
||||
const list = separate(text, tokens);
|
||||
if (match) {
|
||||
return typeof end !== 'undefined' ? list.slice(0, end) : list;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import type { DefaultOptionType, FieldNames, SelectProps } from '../Select';
|
||||
declare function warningProps(props: SelectProps): void;
|
||||
export declare function warningNullOptions(options: DefaultOptionType[], fieldNames: FieldNames): void;
|
||||
export default warningProps;
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import toNodeArray from "@rc-component/util/es/Children/toArray";
|
||||
import warning, { noteOnce } from "@rc-component/util/es/warning";
|
||||
import * as React from 'react';
|
||||
import { isMultiple } from "../BaseSelect";
|
||||
import { toArray } from "./commonUtil";
|
||||
import { convertChildrenToData } from "./legacyUtil";
|
||||
function warningProps(props) {
|
||||
const {
|
||||
mode,
|
||||
options,
|
||||
children,
|
||||
backfill,
|
||||
allowClear,
|
||||
placeholder,
|
||||
getInputElement,
|
||||
showSearch,
|
||||
onSearch,
|
||||
defaultOpen,
|
||||
autoFocus,
|
||||
labelInValue,
|
||||
value,
|
||||
optionLabelProp
|
||||
} = props;
|
||||
const multiple = isMultiple(mode);
|
||||
const mergedShowSearch = showSearch !== undefined ? showSearch : multiple || mode === 'combobox';
|
||||
const mergedOptions = options || convertChildrenToData(children);
|
||||
|
||||
// `tags` should not set option as disabled
|
||||
warning(mode !== 'tags' || mergedOptions.every(opt => !opt.disabled), 'Please avoid setting option to disabled in tags mode since user can always type text as tag.');
|
||||
|
||||
// `combobox` & `tags` should option be `string` type
|
||||
if (mode === 'tags' || mode === 'combobox') {
|
||||
const hasNumberValue = mergedOptions.some(item => {
|
||||
if (item.options) {
|
||||
return item.options.some(opt => typeof ('value' in opt ? opt.value : opt.key) === 'number');
|
||||
}
|
||||
return typeof ('value' in item ? item.value : item.key) === 'number';
|
||||
});
|
||||
warning(!hasNumberValue, '`value` of Option should not use number type when `mode` is `tags` or `combobox`.');
|
||||
}
|
||||
|
||||
// `combobox` should not use `optionLabelProp`
|
||||
warning(mode !== 'combobox' || !optionLabelProp, '`combobox` mode not support `optionLabelProp`. Please set `value` on Option directly.');
|
||||
|
||||
// Only `combobox` support `backfill`
|
||||
warning(mode === 'combobox' || !backfill, '`backfill` only works with `combobox` mode.');
|
||||
|
||||
// Only `combobox` support `getInputElement`
|
||||
warning(mode === 'combobox' || !getInputElement, '`getInputElement` only work with `combobox` mode.');
|
||||
|
||||
// Customize `getInputElement` should not use `allowClear` & `placeholder`
|
||||
noteOnce(mode !== 'combobox' || !getInputElement || !allowClear || !placeholder, 'Customize `getInputElement` should customize clear and placeholder logic instead of configuring `allowClear` and `placeholder`.');
|
||||
|
||||
// `onSearch` should use in `combobox` or `showSearch`
|
||||
if (onSearch && !mergedShowSearch && mode !== 'combobox' && mode !== 'tags') {
|
||||
warning(false, '`onSearch` should work with `showSearch` instead of use alone.');
|
||||
}
|
||||
noteOnce(!defaultOpen || autoFocus, '`defaultOpen` makes Select open without focus which means it will not close by click outside. You can set `autoFocus` if needed.');
|
||||
if (value !== undefined && value !== null) {
|
||||
const values = toArray(value);
|
||||
warning(!labelInValue || values.every(val => typeof val === 'object' && ('key' in val || 'value' in val)), '`value` should in shape of `{ value: string | number, label?: ReactNode }` when you set `labelInValue` to `true`');
|
||||
warning(!multiple || Array.isArray(value), '`value` should be array when `mode` is `multiple` or `tags`');
|
||||
}
|
||||
|
||||
// Syntactic sugar should use correct children type
|
||||
if (children) {
|
||||
let invalidateChildType = null;
|
||||
toNodeArray(children).some(node => {
|
||||
if (! /*#__PURE__*/React.isValidElement(node) || !node.type) {
|
||||
return false;
|
||||
}
|
||||
const {
|
||||
type
|
||||
} = node;
|
||||
if (type.isSelectOption) {
|
||||
return false;
|
||||
}
|
||||
if (type.isSelectOptGroup) {
|
||||
const allChildrenValid = toNodeArray(node.props.children).every(subNode => {
|
||||
if (! /*#__PURE__*/React.isValidElement(subNode) || !node.type || subNode.type.isSelectOption) {
|
||||
return true;
|
||||
}
|
||||
invalidateChildType = subNode.type;
|
||||
return false;
|
||||
});
|
||||
if (allChildrenValid) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
invalidateChildType = type;
|
||||
return true;
|
||||
});
|
||||
if (invalidateChildType) {
|
||||
warning(false, `\`children\` should be \`Select.Option\` or \`Select.OptGroup\` instead of \`${invalidateChildType.displayName || invalidateChildType.name || invalidateChildType}\`.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// value in Select option should not be null
|
||||
// note: OptGroup has options too
|
||||
export function warningNullOptions(options, fieldNames) {
|
||||
if (options) {
|
||||
const recursiveOptions = (optionsList, inGroup = false) => {
|
||||
for (let i = 0; i < optionsList.length; i++) {
|
||||
const option = optionsList[i];
|
||||
if (option[fieldNames?.value] === null) {
|
||||
warning(false, '`value` in Select options should not be `null`.');
|
||||
return true;
|
||||
}
|
||||
if (!inGroup && Array.isArray(option[fieldNames?.options]) && recursiveOptions(option[fieldNames?.options], true)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
recursiveOptions(options);
|
||||
}
|
||||
}
|
||||
export default warningProps;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import type { DisplayValueType } from '.';
|
||||
export interface PoliteProps {
|
||||
visible: boolean;
|
||||
values: DisplayValueType[];
|
||||
}
|
||||
export default function Polite(props: PoliteProps): React.JSX.Element;
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.default = Polite;
|
||||
var React = _interopRequireWildcard(require("react"));
|
||||
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
||||
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
||||
function Polite(props) {
|
||||
const {
|
||||
visible,
|
||||
values
|
||||
} = props;
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only cut part of values since it's a screen reader
|
||||
const MAX_COUNT = 50;
|
||||
return /*#__PURE__*/React.createElement("span", {
|
||||
"aria-live": "polite",
|
||||
style: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
position: 'absolute',
|
||||
overflow: 'hidden',
|
||||
opacity: 0
|
||||
}
|
||||
}, `${values.slice(0, MAX_COUNT).map(({
|
||||
label,
|
||||
value
|
||||
}) => ['number', 'string'].includes(typeof label) ? label : value).join(', ')}`, values.length > MAX_COUNT ? ', ...' : null);
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
import type { AlignType, BuildInPlacements } from '@rc-component/trigger/lib/interface';
|
||||
import type { ScrollConfig, ScrollTo } from '@rc-component/virtual-list/lib/List';
|
||||
import * as React from 'react';
|
||||
import type { DisplayInfoType, DisplayValueType, Mode, Placement, RawValueType, RenderDOMFunc, RenderNode } from '../interface';
|
||||
import type { ComponentsConfig } from '../hooks/useComponents';
|
||||
export type BaseSelectSemanticName = 'prefix' | 'suffix' | 'input' | 'clear' | 'placeholder' | 'content' | 'item' | 'itemContent' | 'itemRemove';
|
||||
/**
|
||||
* ZombieJ:
|
||||
* We are currently refactoring the semantic structure of the component. Changelog:
|
||||
* - Remove `suffixIcon` and change to `suffix`.
|
||||
* - Add `components.root` for replacing response element.
|
||||
* - Remove `getInputElement` and `getRawInputElement` since we can use `components.input` instead.
|
||||
*/
|
||||
export type { DisplayInfoType, DisplayValueType, Mode, Placement, RenderDOMFunc, RenderNode, RawValueType, };
|
||||
export interface RefOptionListProps {
|
||||
onKeyDown: React.KeyboardEventHandler;
|
||||
onKeyUp: React.KeyboardEventHandler;
|
||||
scrollTo?: (args: number | ScrollConfig) => void;
|
||||
}
|
||||
export type CustomTagProps = {
|
||||
label: React.ReactNode;
|
||||
value: any;
|
||||
disabled: boolean;
|
||||
onClose: (event?: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
closable: boolean;
|
||||
isMaxTag: boolean;
|
||||
index: number;
|
||||
};
|
||||
export interface BaseSelectRef {
|
||||
focus: (options?: FocusOptions) => void;
|
||||
blur: () => void;
|
||||
scrollTo: ScrollTo;
|
||||
nativeElement: HTMLElement;
|
||||
}
|
||||
export interface BaseSelectPrivateProps {
|
||||
id: string;
|
||||
prefixCls: string;
|
||||
omitDomProps?: string[];
|
||||
displayValues: DisplayValueType[];
|
||||
onDisplayValuesChange: (values: DisplayValueType[], info: {
|
||||
type: DisplayInfoType;
|
||||
values: DisplayValueType[];
|
||||
}) => void;
|
||||
/** Current dropdown list active item string value */
|
||||
activeValue?: string;
|
||||
/** Link search input with target element */
|
||||
activeDescendantId?: string;
|
||||
onActiveValueChange?: (value: string | null) => void;
|
||||
searchValue: string;
|
||||
autoClearSearchValue?: boolean;
|
||||
/** Trigger onSearch, return false to prevent trigger open event */
|
||||
onSearch: (searchValue: string, info: {
|
||||
source: 'typing' | 'effect' | 'submit' | 'blur';
|
||||
}) => void;
|
||||
/** Trigger when search text match the `tokenSeparators`. Will provide split content */
|
||||
onSearchSplit?: (words: string[]) => void;
|
||||
OptionList: React.ForwardRefExoticComponent<React.PropsWithoutRef<any> & React.RefAttributes<RefOptionListProps>>;
|
||||
/** Tell if provided `options` is empty */
|
||||
emptyOptions: boolean;
|
||||
}
|
||||
export type BaseSelectPropsWithoutPrivate = Omit<BaseSelectProps, keyof BaseSelectPrivateProps>;
|
||||
export interface BaseSelectProps extends BaseSelectPrivateProps, React.AriaAttributes, Pick<React.HTMLAttributes<HTMLElement>, 'role'> {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
classNames?: Partial<Record<BaseSelectSemanticName, string>>;
|
||||
styles?: Partial<Record<BaseSelectSemanticName, React.CSSProperties>>;
|
||||
showSearch?: boolean;
|
||||
tagRender?: (props: CustomTagProps) => React.ReactElement;
|
||||
direction?: 'ltr' | 'rtl';
|
||||
autoFocus?: boolean;
|
||||
placeholder?: React.ReactNode;
|
||||
maxCount?: number;
|
||||
title?: string;
|
||||
tabIndex?: number;
|
||||
notFoundContent?: React.ReactNode;
|
||||
onClear?: () => void;
|
||||
maxLength?: number;
|
||||
showScrollBar?: boolean | 'optional';
|
||||
choiceTransitionName?: string;
|
||||
mode?: Mode;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
open?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
onPopupVisibleChange?: (open: boolean) => void;
|
||||
/** @private Internal usage. Do not use in your production. */
|
||||
getInputElement?: () => JSX.Element;
|
||||
/** @private Internal usage. Do not use in your production. */
|
||||
getRawInputElement?: () => JSX.Element;
|
||||
maxTagTextLength?: number;
|
||||
maxTagCount?: number | 'responsive';
|
||||
maxTagPlaceholder?: React.ReactNode | ((omittedValues: DisplayValueType[]) => React.ReactNode);
|
||||
tokenSeparators?: string[];
|
||||
allowClear?: boolean | {
|
||||
clearIcon?: React.ReactNode;
|
||||
};
|
||||
prefix?: React.ReactNode;
|
||||
/** @deprecated Please use `suffix` instead. */
|
||||
suffixIcon?: RenderNode;
|
||||
suffix?: RenderNode;
|
||||
/**
|
||||
* Clear all icon
|
||||
* @deprecated Please use `allowClear` instead
|
||||
**/
|
||||
clearIcon?: React.ReactNode;
|
||||
/** Selector remove icon */
|
||||
removeIcon?: RenderNode;
|
||||
animation?: string;
|
||||
transitionName?: string;
|
||||
popupStyle?: React.CSSProperties;
|
||||
popupClassName?: string;
|
||||
popupMatchSelectWidth?: boolean | number;
|
||||
popupRender?: (menu: React.ReactElement) => React.ReactElement;
|
||||
popupAlign?: AlignType;
|
||||
placement?: Placement;
|
||||
builtinPlacements?: BuildInPlacements;
|
||||
getPopupContainer?: RenderDOMFunc;
|
||||
showAction?: ('focus' | 'click')[];
|
||||
onBlur?: React.FocusEventHandler<HTMLElement>;
|
||||
onFocus?: React.FocusEventHandler<HTMLElement>;
|
||||
onKeyUp?: React.KeyboardEventHandler<HTMLDivElement>;
|
||||
onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
|
||||
onMouseDown?: React.MouseEventHandler<HTMLDivElement>;
|
||||
onPopupScroll?: React.UIEventHandler<HTMLDivElement>;
|
||||
onInputKeyDown?: React.KeyboardEventHandler<HTMLInputElement | HTMLTextAreaElement>;
|
||||
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
|
||||
onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
components?: ComponentsConfig;
|
||||
}
|
||||
export declare const isMultiple: (mode: Mode) => boolean;
|
||||
declare const BaseSelect: React.ForwardRefExoticComponent<BaseSelectProps & React.RefAttributes<BaseSelectRef>>;
|
||||
export default BaseSelect;
|
||||
+537
@@ -0,0 +1,537 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.isMultiple = exports.default = void 0;
|
||||
var _clsx = require("clsx");
|
||||
var _findDOMNode = require("@rc-component/util/lib/Dom/findDOMNode");
|
||||
var React = _interopRequireWildcard(require("react"));
|
||||
var _useAllowClear = require("../hooks/useAllowClear");
|
||||
var _useBaseProps = require("../hooks/useBaseProps");
|
||||
var _useLock = _interopRequireDefault(require("../hooks/useLock"));
|
||||
var _useSelectTriggerControl = _interopRequireWildcard(require("../hooks/useSelectTriggerControl"));
|
||||
var _SelectTrigger = _interopRequireDefault(require("../SelectTrigger"));
|
||||
var _valueUtil = require("../utils/valueUtil");
|
||||
var _Polite = _interopRequireDefault(require("./Polite"));
|
||||
var _useOpen = _interopRequireWildcard(require("../hooks/useOpen"));
|
||||
var _util = require("@rc-component/util");
|
||||
var _SelectInput = _interopRequireDefault(require("../SelectInput"));
|
||||
var _useComponents = _interopRequireDefault(require("../hooks/useComponents"));
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
||||
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
||||
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
||||
/**
|
||||
* ZombieJ:
|
||||
* We are currently refactoring the semantic structure of the component. Changelog:
|
||||
* - Remove `suffixIcon` and change to `suffix`.
|
||||
* - Add `components.root` for replacing response element.
|
||||
* - Remove `getInputElement` and `getRawInputElement` since we can use `components.input` instead.
|
||||
*/
|
||||
|
||||
const isMultiple = mode => mode === 'tags' || mode === 'multiple';
|
||||
exports.isMultiple = isMultiple;
|
||||
const BaseSelect = /*#__PURE__*/React.forwardRef((props, ref) => {
|
||||
const {
|
||||
id,
|
||||
prefixCls,
|
||||
className,
|
||||
styles,
|
||||
classNames,
|
||||
showSearch,
|
||||
tagRender,
|
||||
showScrollBar = 'optional',
|
||||
direction,
|
||||
omitDomProps,
|
||||
// Value
|
||||
displayValues,
|
||||
onDisplayValuesChange,
|
||||
emptyOptions,
|
||||
notFoundContent = 'Not Found',
|
||||
onClear,
|
||||
maxCount,
|
||||
placeholder,
|
||||
// Mode
|
||||
mode,
|
||||
// Status
|
||||
disabled,
|
||||
loading,
|
||||
// Customize Input
|
||||
getInputElement,
|
||||
getRawInputElement,
|
||||
// Open
|
||||
open,
|
||||
defaultOpen,
|
||||
onPopupVisibleChange,
|
||||
// Active
|
||||
activeValue,
|
||||
onActiveValueChange,
|
||||
activeDescendantId,
|
||||
// Search
|
||||
searchValue,
|
||||
autoClearSearchValue,
|
||||
onSearch,
|
||||
onSearchSplit,
|
||||
tokenSeparators,
|
||||
// Icons
|
||||
allowClear,
|
||||
prefix,
|
||||
suffix,
|
||||
suffixIcon,
|
||||
clearIcon,
|
||||
// Dropdown
|
||||
OptionList,
|
||||
animation,
|
||||
transitionName,
|
||||
popupStyle,
|
||||
popupClassName,
|
||||
popupMatchSelectWidth,
|
||||
popupRender,
|
||||
popupAlign,
|
||||
placement,
|
||||
builtinPlacements,
|
||||
getPopupContainer,
|
||||
// Focus
|
||||
showAction = [],
|
||||
onFocus,
|
||||
onBlur,
|
||||
// Rest Events
|
||||
onKeyUp,
|
||||
onKeyDown,
|
||||
onMouseDown,
|
||||
// Components
|
||||
components,
|
||||
// Rest Props
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
// ============================== MISC ==============================
|
||||
const multiple = isMultiple(mode);
|
||||
|
||||
// ============================== Refs ==============================
|
||||
const containerRef = React.useRef(null);
|
||||
const triggerRef = React.useRef(null);
|
||||
const listRef = React.useRef(null);
|
||||
|
||||
/** Used for component focused management */
|
||||
const [focused, setFocused] = React.useState(false);
|
||||
|
||||
// =========================== Imperative ===========================
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
focus: containerRef.current?.focus,
|
||||
blur: containerRef.current?.blur,
|
||||
scrollTo: arg => listRef.current?.scrollTo(arg),
|
||||
nativeElement: (0, _findDOMNode.getDOM)(containerRef.current)
|
||||
}));
|
||||
|
||||
// =========================== Components ===========================
|
||||
const mergedComponents = (0, _useComponents.default)(components, getInputElement, getRawInputElement);
|
||||
|
||||
// ========================== Search Value ==========================
|
||||
const mergedSearchValue = React.useMemo(() => {
|
||||
if (mode !== 'combobox') {
|
||||
return searchValue;
|
||||
}
|
||||
const val = displayValues[0]?.value;
|
||||
return typeof val === 'string' || typeof val === 'number' ? String(val) : '';
|
||||
}, [searchValue, mode, displayValues]);
|
||||
|
||||
// ========================== Custom Input ==========================
|
||||
// Only works in `combobox`
|
||||
const customizeInputElement = mode === 'combobox' && typeof getInputElement === 'function' && getInputElement() || null;
|
||||
|
||||
// ============================== Open ==============================
|
||||
// Not trigger `open` when `notFoundContent` is empty
|
||||
const emptyListContent = !notFoundContent && emptyOptions;
|
||||
const [rawOpen, mergedOpen, triggerOpen, lockOptions] = (0, _useOpen.default)(defaultOpen || false, open, onPopupVisibleChange, nextOpen => disabled || emptyListContent ? false : nextOpen);
|
||||
|
||||
// ============================= Search =============================
|
||||
const tokenWithEnter = React.useMemo(() => (tokenSeparators || []).some(tokenSeparator => ['\n', '\r\n'].includes(tokenSeparator)), [tokenSeparators]);
|
||||
const onInternalSearch = (searchText, fromTyping, isCompositing) => {
|
||||
if (multiple && (0, _valueUtil.isValidCount)(maxCount) && displayValues.length >= maxCount) {
|
||||
return;
|
||||
}
|
||||
let ret = true;
|
||||
let newSearchText = searchText;
|
||||
onActiveValueChange?.(null);
|
||||
const separatedList = (0, _valueUtil.getSeparatedContent)(searchText, tokenSeparators, (0, _valueUtil.isValidCount)(maxCount) ? maxCount - displayValues.length : undefined);
|
||||
|
||||
// Check if match the `tokenSeparators`
|
||||
const patchLabels = isCompositing ? null : separatedList;
|
||||
|
||||
// Ignore combobox since it's not split-able
|
||||
if (mode !== 'combobox' && patchLabels) {
|
||||
newSearchText = '';
|
||||
onSearchSplit?.(patchLabels);
|
||||
|
||||
// Should close when paste finish
|
||||
triggerOpen(false);
|
||||
|
||||
// Tell Selector that break next actions
|
||||
ret = false;
|
||||
}
|
||||
if (onSearch && mergedSearchValue !== newSearchText) {
|
||||
onSearch(newSearchText, {
|
||||
source: fromTyping ? 'typing' : 'effect'
|
||||
});
|
||||
}
|
||||
|
||||
// Open if from typing
|
||||
if (searchText && fromTyping && ret) {
|
||||
triggerOpen(true);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Only triggered when menu is closed & mode is tags
|
||||
// If menu is open, OptionList will take charge
|
||||
// If mode isn't tags, press enter is not meaningful when you can't see any option
|
||||
const onInternalSearchSubmit = searchText => {
|
||||
// prevent empty tags from appearing when you click the Enter button
|
||||
if (!searchText || !searchText.trim()) {
|
||||
return;
|
||||
}
|
||||
onSearch(searchText, {
|
||||
source: 'submit'
|
||||
});
|
||||
};
|
||||
|
||||
// Clean up search value when the dropdown is closed.
|
||||
// We use `rawOpen` here to avoid clearing the search input when the dropdown is
|
||||
// programmatically closed due to `notFoundContent={null}` and no matching options.
|
||||
// This allows the user to continue typing their search query.
|
||||
React.useEffect(() => {
|
||||
if (!rawOpen && !multiple && mode !== 'combobox') {
|
||||
onInternalSearch('', false, false);
|
||||
}
|
||||
}, [rawOpen]);
|
||||
|
||||
// ============================ Disabled ============================
|
||||
// Close dropdown & remove focus state when disabled change
|
||||
React.useEffect(() => {
|
||||
// After onBlur is triggered, the focused does not need to be reset
|
||||
if (disabled) {
|
||||
triggerOpen(false);
|
||||
setFocused(false);
|
||||
}
|
||||
}, [disabled, mergedOpen]);
|
||||
|
||||
// ============================ Keyboard ============================
|
||||
/**
|
||||
* We record input value here to check if can press to clean up by backspace
|
||||
* - null: Key is not down, this is reset by key up
|
||||
* - true: Search text is empty when first time backspace down
|
||||
* - false: Search text is not empty when first time backspace down
|
||||
*/
|
||||
const [getClearLock, setClearLock] = (0, _useLock.default)();
|
||||
const keyLockRef = React.useRef(false);
|
||||
|
||||
// KeyDown
|
||||
const onInternalKeyDown = event => {
|
||||
const clearLock = getClearLock();
|
||||
const {
|
||||
key
|
||||
} = event;
|
||||
const isEnterKey = key === 'Enter';
|
||||
const isSpaceKey = key === ' ';
|
||||
|
||||
// Enter or Space opens dropdown (ARIA combobox: spacebar should open)
|
||||
if (isEnterKey || isSpaceKey) {
|
||||
// Do not submit form when type in the input; prevent Space from scrolling page
|
||||
const isCombobox = mode === 'combobox';
|
||||
const isEditable = isCombobox || showSearch;
|
||||
if (isSpaceKey && !isEditable || isEnterKey && !isCombobox) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// We only manage open state here, close logic should handle by list component
|
||||
if (!mergedOpen) {
|
||||
triggerOpen(true);
|
||||
}
|
||||
}
|
||||
setClearLock(!!mergedSearchValue);
|
||||
|
||||
// Remove value by `backspace`
|
||||
if (key === 'Backspace' && !clearLock && multiple && !mergedSearchValue && displayValues.length) {
|
||||
const cloneDisplayValues = [...displayValues];
|
||||
let removedDisplayValue = null;
|
||||
for (let i = cloneDisplayValues.length - 1; i >= 0; i -= 1) {
|
||||
const current = cloneDisplayValues[i];
|
||||
if (!current.disabled) {
|
||||
cloneDisplayValues.splice(i, 1);
|
||||
removedDisplayValue = current;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (removedDisplayValue) {
|
||||
onDisplayValuesChange(cloneDisplayValues, {
|
||||
type: 'remove',
|
||||
values: [removedDisplayValue]
|
||||
});
|
||||
}
|
||||
}
|
||||
if (mergedOpen && (!isEnterKey || !keyLockRef.current) && !isSpaceKey) {
|
||||
// Lock the Enter key after it is pressed to avoid repeated triggering of the onChange event.
|
||||
if (isEnterKey) {
|
||||
keyLockRef.current = true;
|
||||
}
|
||||
listRef.current?.onKeyDown(event);
|
||||
}
|
||||
onKeyDown?.(event);
|
||||
};
|
||||
|
||||
// KeyUp
|
||||
const onInternalKeyUp = (event, ...rest) => {
|
||||
if (mergedOpen) {
|
||||
listRef.current?.onKeyUp(event, ...rest);
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
keyLockRef.current = false;
|
||||
}
|
||||
onKeyUp?.(event, ...rest);
|
||||
};
|
||||
|
||||
// ============================ Selector ============================
|
||||
const onSelectorRemove = (0, _util.useEvent)(val => {
|
||||
const newValues = displayValues.filter(i => i !== val);
|
||||
onDisplayValuesChange(newValues, {
|
||||
type: 'remove',
|
||||
values: [val]
|
||||
});
|
||||
});
|
||||
const onInputBlur = () => {
|
||||
// Unlock the Enter key after the input blur; otherwise, the Enter key needs to be pressed twice to trigger the correct effect.
|
||||
keyLockRef.current = false;
|
||||
};
|
||||
|
||||
// ========================== Focus / Blur ==========================
|
||||
const getSelectElements = () => [(0, _findDOMNode.getDOM)(containerRef.current), triggerRef.current?.getPopupElement()];
|
||||
|
||||
// Close when click on non-select element
|
||||
(0, _useSelectTriggerControl.default)(getSelectElements, mergedOpen, triggerOpen, !!mergedComponents.root);
|
||||
|
||||
// ========================== Focus / Blur ==========================
|
||||
const internalMouseDownRef = React.useRef(false);
|
||||
const onInternalFocus = event => {
|
||||
setFocused(true);
|
||||
if (!disabled) {
|
||||
// `showAction` should handle `focus` if set
|
||||
if (showAction.includes('focus')) {
|
||||
triggerOpen(true);
|
||||
}
|
||||
onFocus?.(event);
|
||||
}
|
||||
};
|
||||
const onRootBlur = () => {
|
||||
// Delay close should check the activeElement
|
||||
if (mergedOpen && !internalMouseDownRef.current) {
|
||||
triggerOpen(false, {
|
||||
cancelFun: () => (0, _useSelectTriggerControl.isInside)(getSelectElements(), document.activeElement)
|
||||
});
|
||||
}
|
||||
};
|
||||
const onInternalBlur = event => {
|
||||
setFocused(false);
|
||||
if (mergedSearchValue) {
|
||||
// `tags` mode should move `searchValue` into values
|
||||
if (mode === 'tags') {
|
||||
onSearch(mergedSearchValue, {
|
||||
source: 'submit'
|
||||
});
|
||||
} else if (mode === 'multiple') {
|
||||
// `multiple` mode only clean the search value but not trigger event
|
||||
onSearch('', {
|
||||
source: 'blur'
|
||||
});
|
||||
}
|
||||
}
|
||||
onRootBlur();
|
||||
if (!disabled) {
|
||||
onBlur?.(event);
|
||||
}
|
||||
};
|
||||
const onRootMouseDown = (event, ...restArgs) => {
|
||||
const {
|
||||
target
|
||||
} = event;
|
||||
const popupElement = triggerRef.current?.getPopupElement();
|
||||
|
||||
// We should give focus back to selector if clicked item is not focusable
|
||||
if (popupElement?.contains(target) && triggerOpen) {
|
||||
// Tell `open` not to close since it's safe in the popup
|
||||
triggerOpen(true);
|
||||
}
|
||||
onMouseDown?.(event, ...restArgs);
|
||||
internalMouseDownRef.current = true;
|
||||
(0, _useOpen.macroTask)(() => {
|
||||
internalMouseDownRef.current = false;
|
||||
});
|
||||
};
|
||||
|
||||
// ============================ Dropdown ============================
|
||||
const [, forceUpdate] = React.useState({});
|
||||
// We need force update here since popup dom is render async
|
||||
function onPopupMouseEnter() {
|
||||
forceUpdate({});
|
||||
}
|
||||
|
||||
// Used for raw custom input trigger
|
||||
let onTriggerVisibleChange;
|
||||
if (!!mergedComponents.root) {
|
||||
onTriggerVisibleChange = newOpen => {
|
||||
triggerOpen(newOpen);
|
||||
};
|
||||
}
|
||||
|
||||
// ============================ Context =============================
|
||||
const baseSelectContext = React.useMemo(() => ({
|
||||
...props,
|
||||
notFoundContent,
|
||||
open: mergedOpen,
|
||||
triggerOpen: mergedOpen,
|
||||
rawOpen,
|
||||
id,
|
||||
showSearch,
|
||||
multiple,
|
||||
toggleOpen: triggerOpen,
|
||||
showScrollBar,
|
||||
styles,
|
||||
classNames,
|
||||
lockOptions
|
||||
}), [props, notFoundContent, triggerOpen, id, showSearch, multiple, mergedOpen, rawOpen, showScrollBar, styles, classNames, lockOptions]);
|
||||
|
||||
// ==================================================================
|
||||
// == Render ==
|
||||
// ==================================================================
|
||||
|
||||
// ============================= Suffix =============================
|
||||
const mergedSuffixIcon = React.useMemo(() => {
|
||||
const nextSuffix = suffix ?? suffixIcon;
|
||||
if (typeof nextSuffix === 'function') {
|
||||
return nextSuffix({
|
||||
searchValue: mergedSearchValue,
|
||||
open: mergedOpen,
|
||||
focused,
|
||||
showSearch,
|
||||
loading
|
||||
});
|
||||
}
|
||||
return nextSuffix;
|
||||
}, [suffix, suffixIcon, mergedSearchValue, mergedOpen, focused, showSearch, loading]);
|
||||
|
||||
// ============================= Clear ==============================
|
||||
const onClearMouseDown = () => {
|
||||
onClear?.();
|
||||
containerRef.current?.focus();
|
||||
onDisplayValuesChange([], {
|
||||
type: 'clear',
|
||||
values: displayValues
|
||||
});
|
||||
onInternalSearch('', false, false);
|
||||
};
|
||||
const {
|
||||
allowClear: mergedAllowClear,
|
||||
clearIcon: clearNode
|
||||
} = (0, _useAllowClear.useAllowClear)(prefixCls, displayValues, allowClear, clearIcon, disabled, mergedSearchValue, mode);
|
||||
|
||||
// =========================== OptionList ===========================
|
||||
const optionList = /*#__PURE__*/React.createElement(OptionList, {
|
||||
ref: listRef
|
||||
});
|
||||
|
||||
// ============================= Select =============================
|
||||
const mergedClassName = (0, _clsx.clsx)(prefixCls, className, {
|
||||
[`${prefixCls}-focused`]: focused,
|
||||
[`${prefixCls}-multiple`]: multiple,
|
||||
[`${prefixCls}-single`]: !multiple,
|
||||
[`${prefixCls}-allow-clear`]: mergedAllowClear,
|
||||
[`${prefixCls}-show-arrow`]: mergedSuffixIcon !== undefined && mergedSuffixIcon !== null,
|
||||
[`${prefixCls}-disabled`]: disabled,
|
||||
[`${prefixCls}-loading`]: loading,
|
||||
[`${prefixCls}-open`]: mergedOpen,
|
||||
[`${prefixCls}-customize-input`]: customizeInputElement,
|
||||
[`${prefixCls}-show-search`]: showSearch
|
||||
});
|
||||
|
||||
// >>> Render
|
||||
let renderNode = /*#__PURE__*/React.createElement(_SelectInput.default, _extends({}, restProps, {
|
||||
// Ref
|
||||
ref: containerRef
|
||||
// Style
|
||||
,
|
||||
prefixCls: prefixCls,
|
||||
className: mergedClassName
|
||||
// Focus state
|
||||
,
|
||||
focused: focused
|
||||
// UI
|
||||
,
|
||||
prefix: prefix,
|
||||
suffix: mergedSuffixIcon,
|
||||
clearIcon: clearNode
|
||||
// Type or mode
|
||||
,
|
||||
multiple: multiple,
|
||||
mode: mode
|
||||
// Values
|
||||
,
|
||||
displayValues: displayValues,
|
||||
placeholder: placeholder,
|
||||
searchValue: mergedSearchValue,
|
||||
activeValue: activeValue,
|
||||
onSearch: onInternalSearch,
|
||||
onSearchSubmit: onInternalSearchSubmit,
|
||||
onInputBlur: onInputBlur,
|
||||
onFocus: onInternalFocus,
|
||||
onBlur: onInternalBlur,
|
||||
onClearMouseDown: onClearMouseDown,
|
||||
onKeyDown: onInternalKeyDown,
|
||||
onKeyUp: onInternalKeyUp,
|
||||
onSelectorRemove: onSelectorRemove
|
||||
// Token handling
|
||||
,
|
||||
tokenWithEnter: tokenWithEnter
|
||||
// Open
|
||||
,
|
||||
onMouseDown: onRootMouseDown
|
||||
// Components
|
||||
,
|
||||
components: mergedComponents
|
||||
}));
|
||||
renderNode = /*#__PURE__*/React.createElement(_SelectTrigger.default, {
|
||||
ref: triggerRef,
|
||||
disabled: disabled,
|
||||
prefixCls: prefixCls,
|
||||
visible: mergedOpen,
|
||||
popupElement: optionList,
|
||||
animation: animation,
|
||||
transitionName: transitionName,
|
||||
popupStyle: popupStyle,
|
||||
popupClassName: popupClassName,
|
||||
direction: direction,
|
||||
popupMatchSelectWidth: popupMatchSelectWidth,
|
||||
popupRender: popupRender,
|
||||
popupAlign: popupAlign,
|
||||
placement: placement,
|
||||
builtinPlacements: builtinPlacements,
|
||||
getPopupContainer: getPopupContainer,
|
||||
empty: emptyOptions,
|
||||
onPopupVisibleChange: onTriggerVisibleChange,
|
||||
onPopupMouseEnter: onPopupMouseEnter,
|
||||
onPopupMouseDown: onRootMouseDown,
|
||||
onPopupBlur: onRootBlur
|
||||
}, renderNode);
|
||||
return /*#__PURE__*/React.createElement(_useBaseProps.BaseSelectContext.Provider, {
|
||||
value: baseSelectContext
|
||||
}, /*#__PURE__*/React.createElement(_Polite.default, {
|
||||
visible: focused && !mergedOpen,
|
||||
values: displayValues
|
||||
}), renderNode);
|
||||
});
|
||||
|
||||
// Set display name for dev
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
BaseSelect.displayName = 'BaseSelect';
|
||||
}
|
||||
var _default = exports.default = BaseSelect;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import type * as React from 'react';
|
||||
import type { DefaultOptionType } from './Select';
|
||||
export interface OptGroupProps extends Omit<DefaultOptionType, 'options'> {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
export interface OptionGroupFC extends React.FC<OptGroupProps> {
|
||||
/** Legacy for check if is a Option Group */
|
||||
isSelectOptGroup: boolean;
|
||||
}
|
||||
/** This is a placeholder, not real render in dom */
|
||||
declare const OptGroup: OptionGroupFC;
|
||||
export default OptGroup;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.default = void 0;
|
||||
/* istanbul ignore file */
|
||||
|
||||
/** This is a placeholder, not real render in dom */
|
||||
const OptGroup = () => null;
|
||||
OptGroup.isSelectOptGroup = true;
|
||||
var _default = exports.default = OptGroup;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import type * as React from 'react';
|
||||
import type { DefaultOptionType } from './Select';
|
||||
export interface OptionProps extends Omit<DefaultOptionType, 'label'> {
|
||||
children: React.ReactNode;
|
||||
/** Save for customize data */
|
||||
[prop: string]: any;
|
||||
}
|
||||
export interface OptionFC extends React.FC<OptionProps> {
|
||||
/** Legacy for check if is a Option Group */
|
||||
isSelectOption: boolean;
|
||||
}
|
||||
/** This is a placeholder, not real render in dom */
|
||||
declare const Option: OptionFC;
|
||||
export default Option;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.default = void 0;
|
||||
/* istanbul ignore file */
|
||||
|
||||
/** This is a placeholder, not real render in dom */
|
||||
const Option = () => null;
|
||||
Option.isSelectOption = true;
|
||||
var _default = exports.default = Option;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import type { ScrollConfig } from '@rc-component/virtual-list/lib/List';
|
||||
import * as React from 'react';
|
||||
export type OptionListProps = Record<string, never>;
|
||||
export interface RefOptionListProps {
|
||||
onKeyDown: React.KeyboardEventHandler;
|
||||
onKeyUp: React.KeyboardEventHandler;
|
||||
scrollTo?: (args: number | ScrollConfig) => void;
|
||||
}
|
||||
declare const RefOptionList: React.ForwardRefExoticComponent<React.RefAttributes<RefOptionListProps>>;
|
||||
export default RefOptionList;
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.default = void 0;
|
||||
var _clsx = require("clsx");
|
||||
var _KeyCode = _interopRequireDefault(require("@rc-component/util/lib/KeyCode"));
|
||||
var _useMemo = _interopRequireDefault(require("@rc-component/util/lib/hooks/useMemo"));
|
||||
var _omit = _interopRequireDefault(require("@rc-component/util/lib/omit"));
|
||||
var _pickAttrs = _interopRequireDefault(require("@rc-component/util/lib/pickAttrs"));
|
||||
var _virtualList = _interopRequireDefault(require("@rc-component/virtual-list"));
|
||||
var _react = _interopRequireWildcard(require("react"));
|
||||
var React = _react;
|
||||
var _SelectContext = _interopRequireDefault(require("./SelectContext"));
|
||||
var _TransBtn = _interopRequireDefault(require("./TransBtn"));
|
||||
var _useBaseProps = _interopRequireDefault(require("./hooks/useBaseProps"));
|
||||
var _platformUtil = require("./utils/platformUtil");
|
||||
var _valueUtil = require("./utils/valueUtil");
|
||||
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
||||
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
||||
// export interface OptionListProps<OptionsType extends object[]> {
|
||||
|
||||
function isTitleType(content) {
|
||||
return typeof content === 'string' || typeof content === 'number';
|
||||
}
|
||||
|
||||
/**
|
||||
* Using virtual list of option display.
|
||||
* Will fallback to dom if use customize render.
|
||||
*/
|
||||
const OptionList = (_, ref) => {
|
||||
const {
|
||||
prefixCls,
|
||||
id,
|
||||
open,
|
||||
multiple,
|
||||
mode,
|
||||
searchValue,
|
||||
toggleOpen,
|
||||
notFoundContent,
|
||||
onPopupScroll,
|
||||
showScrollBar,
|
||||
lockOptions
|
||||
} = (0, _useBaseProps.default)();
|
||||
const {
|
||||
maxCount,
|
||||
flattenOptions,
|
||||
onActiveValue,
|
||||
defaultActiveFirstOption,
|
||||
onSelect,
|
||||
menuItemSelectedIcon,
|
||||
rawValues,
|
||||
fieldNames,
|
||||
virtual,
|
||||
direction,
|
||||
listHeight,
|
||||
listItemHeight,
|
||||
optionRender,
|
||||
classNames: contextClassNames,
|
||||
styles: contextStyles
|
||||
} = React.useContext(_SelectContext.default);
|
||||
const itemPrefixCls = `${prefixCls}-item`;
|
||||
const memoFlattenOptions = (0, _useMemo.default)(() => flattenOptions, [open, lockOptions], (prev, next) => next[0] && !next[1]);
|
||||
|
||||
// =========================== List ===========================
|
||||
const listRef = React.useRef(null);
|
||||
const overMaxCount = React.useMemo(() => multiple && (0, _valueUtil.isValidCount)(maxCount) && rawValues?.size >= maxCount, [multiple, maxCount, rawValues?.size]);
|
||||
const onListMouseDown = event => {
|
||||
event.preventDefault();
|
||||
};
|
||||
const scrollIntoView = args => {
|
||||
listRef.current?.scrollTo(typeof args === 'number' ? {
|
||||
index: args
|
||||
} : args);
|
||||
};
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/34975
|
||||
const isSelected = React.useCallback(value => {
|
||||
if (mode === 'combobox') {
|
||||
return false;
|
||||
}
|
||||
return rawValues.has(value);
|
||||
}, [mode, [...rawValues].toString(), rawValues.size]);
|
||||
|
||||
// ========================== Active ==========================
|
||||
const getEnabledActiveIndex = (index, offset = 1) => {
|
||||
const len = memoFlattenOptions.length;
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
const current = (index + i * offset + len) % len;
|
||||
const {
|
||||
group,
|
||||
data
|
||||
} = memoFlattenOptions[current] || {};
|
||||
if (!group && !data?.disabled && (isSelected(data.value) || !overMaxCount)) {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
const [activeIndex, setActiveIndex] = React.useState(() => getEnabledActiveIndex(0));
|
||||
const setActive = (index, fromKeyboard = false) => {
|
||||
setActiveIndex(index);
|
||||
const info = {
|
||||
source: fromKeyboard ? 'keyboard' : 'mouse'
|
||||
};
|
||||
|
||||
// Trigger active event
|
||||
const flattenItem = memoFlattenOptions[index];
|
||||
if (!flattenItem) {
|
||||
onActiveValue(null, -1, info);
|
||||
return;
|
||||
}
|
||||
onActiveValue(flattenItem.value, index, info);
|
||||
};
|
||||
|
||||
// Auto active first item when list length or searchValue changed
|
||||
(0, _react.useEffect)(() => {
|
||||
setActive(defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1);
|
||||
}, [memoFlattenOptions.length, searchValue]);
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/48036
|
||||
const isAriaSelected = React.useCallback(value => {
|
||||
if (mode === 'combobox') {
|
||||
return String(value).toLowerCase() === searchValue.toLowerCase();
|
||||
}
|
||||
return rawValues.has(value);
|
||||
}, [mode, searchValue, [...rawValues].toString(), rawValues.size]);
|
||||
|
||||
// Auto scroll to item position in single mode
|
||||
(0, _react.useEffect)(() => {
|
||||
/**
|
||||
* React will skip `onChange` when component update.
|
||||
* `setActive` function will call root accessibility state update which makes re-render.
|
||||
* So we need to delay to let Input component trigger onChange first.
|
||||
*/
|
||||
let timeoutId;
|
||||
if (!multiple && open && rawValues.size === 1) {
|
||||
const value = Array.from(rawValues)[0];
|
||||
// Scroll to the option closest to the searchValue if searching.
|
||||
const index = memoFlattenOptions.findIndex(({
|
||||
data
|
||||
}) => searchValue ? String(data.value).startsWith(searchValue) : data.value === value);
|
||||
if (index !== -1) {
|
||||
setActive(index);
|
||||
timeoutId = setTimeout(() => {
|
||||
scrollIntoView(index);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Force trigger scrollbar visible when open
|
||||
if (open) {
|
||||
listRef.current?.scrollTo(undefined);
|
||||
}
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [open, searchValue]);
|
||||
|
||||
// ========================== Values ==========================
|
||||
const onSelectValue = value => {
|
||||
if (value !== undefined) {
|
||||
onSelect(value, {
|
||||
selected: !rawValues.has(value)
|
||||
});
|
||||
}
|
||||
|
||||
// Single mode should always close by select
|
||||
if (!multiple) {
|
||||
toggleOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ========================= Keyboard =========================
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
onKeyDown: event => {
|
||||
const {
|
||||
which,
|
||||
ctrlKey
|
||||
} = event;
|
||||
switch (which) {
|
||||
// >>> Arrow keys & ctrl + n/p on Mac
|
||||
case _KeyCode.default.N:
|
||||
case _KeyCode.default.P:
|
||||
case _KeyCode.default.UP:
|
||||
case _KeyCode.default.DOWN:
|
||||
{
|
||||
let offset = 0;
|
||||
if (which === _KeyCode.default.UP) {
|
||||
offset = -1;
|
||||
} else if (which === _KeyCode.default.DOWN) {
|
||||
offset = 1;
|
||||
} else if ((0, _platformUtil.isPlatformMac)() && ctrlKey) {
|
||||
if (which === _KeyCode.default.N) {
|
||||
offset = 1;
|
||||
} else if (which === _KeyCode.default.P) {
|
||||
offset = -1;
|
||||
}
|
||||
}
|
||||
if (offset !== 0) {
|
||||
const nextActiveIndex = getEnabledActiveIndex(activeIndex + offset, offset);
|
||||
scrollIntoView(nextActiveIndex);
|
||||
setActive(nextActiveIndex, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// >>> Select (Tab / Enter)
|
||||
case _KeyCode.default.TAB:
|
||||
case _KeyCode.default.ENTER:
|
||||
{
|
||||
// value
|
||||
const item = memoFlattenOptions[activeIndex];
|
||||
if (!item || item.data.disabled) {
|
||||
return onSelectValue(undefined);
|
||||
}
|
||||
if (!overMaxCount || rawValues.has(item.value)) {
|
||||
onSelectValue(item.value);
|
||||
} else {
|
||||
onSelectValue(undefined);
|
||||
}
|
||||
if (open) {
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// >>> Close
|
||||
case _KeyCode.default.ESC:
|
||||
{
|
||||
toggleOpen(false);
|
||||
if (open) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onKeyUp: () => {},
|
||||
scrollTo: index => {
|
||||
scrollIntoView(index);
|
||||
}
|
||||
}));
|
||||
|
||||
// ========================== Render ==========================
|
||||
if (memoFlattenOptions.length === 0) {
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
role: "listbox",
|
||||
id: `${id}_list`,
|
||||
className: `${itemPrefixCls}-empty`,
|
||||
onMouseDown: onListMouseDown
|
||||
}, notFoundContent);
|
||||
}
|
||||
const omitFieldNameList = Object.keys(fieldNames).map(key => fieldNames[key]);
|
||||
const getLabel = item => item.label;
|
||||
function getItemAriaProps(item, index) {
|
||||
const {
|
||||
group
|
||||
} = item;
|
||||
return {
|
||||
role: group ? 'presentation' : 'option',
|
||||
id: `${id}_list_${index}`
|
||||
};
|
||||
}
|
||||
const renderItem = index => {
|
||||
const item = memoFlattenOptions[index];
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
const itemData = item.data || {};
|
||||
const {
|
||||
value,
|
||||
disabled
|
||||
} = itemData;
|
||||
const {
|
||||
group
|
||||
} = item;
|
||||
const attrs = (0, _pickAttrs.default)(itemData, true);
|
||||
const mergedLabel = getLabel(item);
|
||||
return item ? /*#__PURE__*/React.createElement("div", _extends({
|
||||
"aria-label": typeof mergedLabel === 'string' && !group ? mergedLabel : null
|
||||
}, attrs, {
|
||||
key: index
|
||||
}, getItemAriaProps(item, index), {
|
||||
"aria-selected": isAriaSelected(value),
|
||||
"aria-disabled": disabled
|
||||
}), value) : null;
|
||||
};
|
||||
const a11yProps = {
|
||||
role: 'listbox',
|
||||
id: `${id}_list`
|
||||
};
|
||||
return /*#__PURE__*/React.createElement(React.Fragment, null, virtual && /*#__PURE__*/React.createElement("div", _extends({}, a11yProps, {
|
||||
style: {
|
||||
height: 0,
|
||||
width: 0,
|
||||
overflow: 'hidden'
|
||||
}
|
||||
}), renderItem(activeIndex - 1), renderItem(activeIndex), renderItem(activeIndex + 1)), /*#__PURE__*/React.createElement(_virtualList.default, {
|
||||
itemKey: "key",
|
||||
ref: listRef,
|
||||
data: memoFlattenOptions,
|
||||
height: listHeight,
|
||||
itemHeight: listItemHeight,
|
||||
fullHeight: false,
|
||||
onMouseDown: onListMouseDown,
|
||||
onScroll: onPopupScroll,
|
||||
virtual: virtual,
|
||||
direction: direction,
|
||||
innerProps: virtual ? null : a11yProps,
|
||||
showScrollBar: showScrollBar,
|
||||
className: contextClassNames?.popup?.list,
|
||||
style: contextStyles?.popup?.list
|
||||
}, (item, itemIndex) => {
|
||||
const {
|
||||
group,
|
||||
groupOption,
|
||||
data,
|
||||
label,
|
||||
value
|
||||
} = item;
|
||||
const {
|
||||
key
|
||||
} = data;
|
||||
|
||||
// Group
|
||||
if (group) {
|
||||
const groupTitle = data.title ?? (isTitleType(label) ? label.toString() : undefined);
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
className: (0, _clsx.clsx)(itemPrefixCls, `${itemPrefixCls}-group`, data.className),
|
||||
title: groupTitle
|
||||
}, label !== undefined ? label : key);
|
||||
}
|
||||
const {
|
||||
disabled,
|
||||
title,
|
||||
children,
|
||||
style,
|
||||
className,
|
||||
...otherProps
|
||||
} = data;
|
||||
const passedProps = (0, _omit.default)(otherProps, omitFieldNameList);
|
||||
|
||||
// Option
|
||||
const selected = isSelected(value);
|
||||
const mergedDisabled = disabled || !selected && overMaxCount;
|
||||
const optionPrefixCls = `${itemPrefixCls}-option`;
|
||||
const optionClassName = (0, _clsx.clsx)(itemPrefixCls, optionPrefixCls, className, contextClassNames?.popup?.listItem, {
|
||||
[`${optionPrefixCls}-grouped`]: groupOption,
|
||||
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !mergedDisabled,
|
||||
[`${optionPrefixCls}-disabled`]: mergedDisabled,
|
||||
[`${optionPrefixCls}-selected`]: selected
|
||||
});
|
||||
const mergedLabel = getLabel(item);
|
||||
const iconVisible = !menuItemSelectedIcon || typeof menuItemSelectedIcon === 'function' || selected;
|
||||
|
||||
// https://github.com/ant-design/ant-design/issues/34145
|
||||
const content = typeof mergedLabel === 'number' ? mergedLabel : mergedLabel || value;
|
||||
// https://github.com/ant-design/ant-design/issues/26717
|
||||
let optionTitle = isTitleType(content) ? content.toString() : undefined;
|
||||
if (title !== undefined) {
|
||||
optionTitle = title;
|
||||
}
|
||||
return /*#__PURE__*/React.createElement("div", _extends({}, (0, _pickAttrs.default)(passedProps), !virtual ? getItemAriaProps(item, itemIndex) : {}, {
|
||||
"aria-selected": virtual ? undefined : isAriaSelected(value),
|
||||
"aria-disabled": mergedDisabled,
|
||||
className: optionClassName,
|
||||
title: optionTitle,
|
||||
onMouseMove: () => {
|
||||
if (activeIndex === itemIndex || mergedDisabled) {
|
||||
return;
|
||||
}
|
||||
setActive(itemIndex);
|
||||
},
|
||||
onClick: () => {
|
||||
if (!mergedDisabled) {
|
||||
onSelectValue(value);
|
||||
}
|
||||
},
|
||||
style: {
|
||||
...contextStyles?.popup?.listItem,
|
||||
...style
|
||||
}
|
||||
}), /*#__PURE__*/React.createElement("div", {
|
||||
className: `${optionPrefixCls}-content`
|
||||
}, typeof optionRender === 'function' ? optionRender(item, {
|
||||
index: itemIndex
|
||||
}) : content), /*#__PURE__*/React.isValidElement(menuItemSelectedIcon) || selected, iconVisible && /*#__PURE__*/React.createElement(_TransBtn.default, {
|
||||
className: `${itemPrefixCls}-option-state`,
|
||||
customizeIcon: menuItemSelectedIcon,
|
||||
customizeIconProps: {
|
||||
value,
|
||||
disabled: mergedDisabled,
|
||||
isSelected: selected
|
||||
}
|
||||
}, selected ? '✓' : null));
|
||||
}));
|
||||
};
|
||||
const RefOptionList = /*#__PURE__*/React.forwardRef(OptionList);
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
RefOptionList.displayName = 'OptionList';
|
||||
}
|
||||
var _default = exports.default = RefOptionList;
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* To match accessibility requirement, we always provide an input in the component.
|
||||
* Other element will not set `tabIndex` to avoid `onBlur` sequence problem.
|
||||
* For focused select, we set `aria-live="polite"` to update the accessibility content.
|
||||
*
|
||||
* ref:
|
||||
* - keyboard: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role#Keyboard_interactions
|
||||
*
|
||||
* New api:
|
||||
* - listHeight
|
||||
* - listItemHeight
|
||||
* - component
|
||||
*
|
||||
* Remove deprecated api:
|
||||
* - multiple
|
||||
* - tags
|
||||
* - combobox
|
||||
* - firstActiveValue
|
||||
* - dropdownMenuStyle
|
||||
* - openClassName (Not list in api)
|
||||
*
|
||||
* Update:
|
||||
* - `backfill` only support `combobox` mode
|
||||
* - `combobox` mode not support `labelInValue` since it's meaningless
|
||||
* - `getInputElement` only support `combobox` mode
|
||||
* - `onChange` return OptionData instead of ReactNode
|
||||
* - `filterOption` `onChange` `onSelect` accept OptionData instead of ReactNode
|
||||
* - `combobox` mode trigger `onChange` will get `undefined` if no `value` match in Option
|
||||
* - `combobox` mode not support `optionLabelProp`
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import type { BaseSelectPropsWithoutPrivate, BaseSelectRef, BaseSelectSemanticName, DisplayValueType, RenderNode } from './BaseSelect';
|
||||
import OptGroup from './OptGroup';
|
||||
import Option from './Option';
|
||||
import type { FlattenOptionData } from './interface';
|
||||
export type OnActiveValue = (active: RawValueType, index: number, info?: {
|
||||
source?: 'keyboard' | 'mouse';
|
||||
}) => void;
|
||||
export type OnInternalSelect = (value: RawValueType, info: {
|
||||
selected: boolean;
|
||||
}) => void;
|
||||
export type RawValueType = string | number;
|
||||
export interface LabelInValueType {
|
||||
label: React.ReactNode;
|
||||
value: RawValueType;
|
||||
}
|
||||
export type DraftValueType = RawValueType | LabelInValueType | DisplayValueType | (RawValueType | LabelInValueType | DisplayValueType)[];
|
||||
export type FilterFunc<OptionType> = (inputValue: string, option?: OptionType) => boolean;
|
||||
export interface FieldNames {
|
||||
value?: string;
|
||||
label?: string;
|
||||
groupLabel?: string;
|
||||
options?: string;
|
||||
}
|
||||
export interface BaseOptionType {
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
title?: string;
|
||||
[name: string]: any;
|
||||
}
|
||||
export interface DefaultOptionType extends BaseOptionType {
|
||||
label?: React.ReactNode;
|
||||
value?: string | number | null;
|
||||
children?: Omit<DefaultOptionType, 'children'>[];
|
||||
}
|
||||
export type SelectHandler<ValueType, OptionType extends BaseOptionType = DefaultOptionType> = (value: ValueType, option: OptionType) => void;
|
||||
type ArrayElementType<T> = T extends (infer E)[] ? E : T;
|
||||
export type SemanticName = BaseSelectSemanticName;
|
||||
export type PopupSemantic = 'listItem' | 'list';
|
||||
export interface SearchConfig<OptionType> {
|
||||
searchValue?: string;
|
||||
autoClearSearchValue?: boolean;
|
||||
onSearch?: (value: string) => void;
|
||||
filterOption?: boolean | FilterFunc<OptionType>;
|
||||
filterSort?: (optionA: OptionType, optionB: OptionType, info: {
|
||||
searchValue: string;
|
||||
}) => number;
|
||||
optionFilterProp?: string | string[];
|
||||
}
|
||||
export interface SelectProps<ValueType = any, OptionType extends BaseOptionType = DefaultOptionType> extends Omit<BaseSelectPropsWithoutPrivate, 'showSearch'> {
|
||||
prefixCls?: string;
|
||||
id?: string;
|
||||
backfill?: boolean;
|
||||
fieldNames?: FieldNames;
|
||||
/** @deprecated please use showSearch.onSearch */
|
||||
onSearch?: SearchConfig<OptionType>['onSearch'];
|
||||
showSearch?: boolean | SearchConfig<OptionType>;
|
||||
/** @deprecated please use showSearch.searchValue */
|
||||
searchValue?: SearchConfig<OptionType>['searchValue'];
|
||||
/** @deprecated please use showSearch.autoClearSearchValue */
|
||||
autoClearSearchValue?: boolean;
|
||||
onSelect?: SelectHandler<ArrayElementType<ValueType>, OptionType>;
|
||||
onDeselect?: SelectHandler<ArrayElementType<ValueType>, OptionType>;
|
||||
onActive?: (value: ValueType) => void;
|
||||
/**
|
||||
* In Select, `false` means do nothing.
|
||||
* In TreeSelect, `false` will highlight match item.
|
||||
* It's by design.
|
||||
*/
|
||||
/** @deprecated please use showSearch.filterOption */
|
||||
filterOption?: SearchConfig<OptionType>['filterOption'];
|
||||
/** @deprecated please use showSearch.filterSort */
|
||||
filterSort?: SearchConfig<OptionType>['filterSort'];
|
||||
/** @deprecated please use showSearch.optionFilterProp */
|
||||
optionFilterProp?: string | string[];
|
||||
optionLabelProp?: string;
|
||||
children?: React.ReactNode;
|
||||
options?: OptionType[];
|
||||
optionRender?: (oriOption: FlattenOptionData<OptionType>, info: {
|
||||
index: number;
|
||||
}) => React.ReactNode;
|
||||
defaultActiveFirstOption?: boolean;
|
||||
virtual?: boolean;
|
||||
direction?: 'ltr' | 'rtl';
|
||||
listHeight?: number;
|
||||
listItemHeight?: number;
|
||||
labelRender?: (props: LabelInValueType) => React.ReactNode;
|
||||
menuItemSelectedIcon?: RenderNode;
|
||||
mode?: 'combobox' | 'multiple' | 'tags';
|
||||
labelInValue?: boolean;
|
||||
value?: ValueType | null;
|
||||
defaultValue?: ValueType | null;
|
||||
maxCount?: number;
|
||||
onChange?: (value: ValueType, option?: OptionType | OptionType[]) => void;
|
||||
classNames?: Partial<Record<SemanticName, string>>;
|
||||
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
|
||||
}
|
||||
declare const TypedSelect: (<ValueType = any, OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType>(props: React.PropsWithChildren<SelectProps<ValueType, OptionType>> & React.RefAttributes<BaseSelectRef>) => React.ReactElement) & {
|
||||
Option: typeof Option;
|
||||
OptGroup: typeof OptGroup;
|
||||
};
|
||||
export default TypedSelect;
|
||||
+532
@@ -0,0 +1,532 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.default = void 0;
|
||||
var _useControlledState = _interopRequireDefault(require("@rc-component/util/lib/hooks/useControlledState"));
|
||||
var _warning = _interopRequireDefault(require("@rc-component/util/lib/warning"));
|
||||
var React = _interopRequireWildcard(require("react"));
|
||||
var _BaseSelect = _interopRequireWildcard(require("./BaseSelect"));
|
||||
var _OptGroup = _interopRequireDefault(require("./OptGroup"));
|
||||
var _Option = _interopRequireDefault(require("./Option"));
|
||||
var _OptionList = _interopRequireDefault(require("./OptionList"));
|
||||
var _SelectContext = _interopRequireDefault(require("./SelectContext"));
|
||||
var _useCache = _interopRequireDefault(require("./hooks/useCache"));
|
||||
var _useFilterOptions = _interopRequireDefault(require("./hooks/useFilterOptions"));
|
||||
var _useId = _interopRequireDefault(require("@rc-component/util/lib/hooks/useId"));
|
||||
var _useOptions = _interopRequireDefault(require("./hooks/useOptions"));
|
||||
var _useRefFunc = _interopRequireDefault(require("./hooks/useRefFunc"));
|
||||
var _commonUtil = require("./utils/commonUtil");
|
||||
var _valueUtil = require("./utils/valueUtil");
|
||||
var _warningPropsUtil = _interopRequireWildcard(require("./utils/warningPropsUtil"));
|
||||
var _useSearchConfig = _interopRequireDefault(require("./hooks/useSearchConfig"));
|
||||
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
||||
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } /**
|
||||
* To match accessibility requirement, we always provide an input in the component.
|
||||
* Other element will not set `tabIndex` to avoid `onBlur` sequence problem.
|
||||
* For focused select, we set `aria-live="polite"` to update the accessibility content.
|
||||
*
|
||||
* ref:
|
||||
* - keyboard: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role#Keyboard_interactions
|
||||
*
|
||||
* New api:
|
||||
* - listHeight
|
||||
* - listItemHeight
|
||||
* - component
|
||||
*
|
||||
* Remove deprecated api:
|
||||
* - multiple
|
||||
* - tags
|
||||
* - combobox
|
||||
* - firstActiveValue
|
||||
* - dropdownMenuStyle
|
||||
* - openClassName (Not list in api)
|
||||
*
|
||||
* Update:
|
||||
* - `backfill` only support `combobox` mode
|
||||
* - `combobox` mode not support `labelInValue` since it's meaningless
|
||||
* - `getInputElement` only support `combobox` mode
|
||||
* - `onChange` return OptionData instead of ReactNode
|
||||
* - `filterOption` `onChange` `onSelect` accept OptionData instead of ReactNode
|
||||
* - `combobox` mode trigger `onChange` will get `undefined` if no `value` match in Option
|
||||
* - `combobox` mode not support `optionLabelProp`
|
||||
*/
|
||||
const OMIT_DOM_PROPS = ['inputValue'];
|
||||
function isRawValue(value) {
|
||||
return !value || typeof value !== 'object';
|
||||
}
|
||||
const Select = /*#__PURE__*/React.forwardRef((props, ref) => {
|
||||
const {
|
||||
id,
|
||||
mode,
|
||||
prefixCls = 'rc-select',
|
||||
backfill,
|
||||
fieldNames,
|
||||
// Search
|
||||
showSearch,
|
||||
searchValue: legacySearchValue,
|
||||
onSearch: legacyOnSearch,
|
||||
autoClearSearchValue: legacyAutoClearSearchValue,
|
||||
filterOption: legacyFilterOption,
|
||||
optionFilterProp: legacyOptionFilterProp,
|
||||
filterSort: legacyFilterSort,
|
||||
// Select
|
||||
onSelect,
|
||||
onDeselect,
|
||||
onActive,
|
||||
popupMatchSelectWidth = true,
|
||||
optionLabelProp,
|
||||
options,
|
||||
optionRender,
|
||||
children,
|
||||
defaultActiveFirstOption,
|
||||
menuItemSelectedIcon,
|
||||
virtual,
|
||||
direction,
|
||||
listHeight = 200,
|
||||
listItemHeight = 20,
|
||||
labelRender,
|
||||
// Value
|
||||
value,
|
||||
defaultValue,
|
||||
labelInValue,
|
||||
onChange,
|
||||
maxCount,
|
||||
classNames,
|
||||
styles,
|
||||
...restProps
|
||||
} = props;
|
||||
const searchProps = {
|
||||
searchValue: legacySearchValue,
|
||||
onSearch: legacyOnSearch,
|
||||
autoClearSearchValue: legacyAutoClearSearchValue,
|
||||
filterOption: legacyFilterOption,
|
||||
optionFilterProp: legacyOptionFilterProp,
|
||||
filterSort: legacyFilterSort
|
||||
};
|
||||
const [mergedShowSearch, searchConfig] = (0, _useSearchConfig.default)(showSearch, searchProps, mode);
|
||||
const {
|
||||
filterOption,
|
||||
searchValue,
|
||||
optionFilterProp,
|
||||
filterSort,
|
||||
onSearch,
|
||||
autoClearSearchValue = true
|
||||
} = searchConfig;
|
||||
const normalizedOptionFilterProp = React.useMemo(() => {
|
||||
if (!optionFilterProp) return [];
|
||||
return Array.isArray(optionFilterProp) ? optionFilterProp : [optionFilterProp];
|
||||
}, [optionFilterProp]);
|
||||
const mergedId = (0, _useId.default)(id);
|
||||
const multiple = (0, _BaseSelect.isMultiple)(mode);
|
||||
const childrenAsData = !!(!options && children);
|
||||
const mergedFilterOption = React.useMemo(() => {
|
||||
if (filterOption === undefined && mode === 'combobox') {
|
||||
return false;
|
||||
}
|
||||
return filterOption;
|
||||
}, [filterOption, mode]);
|
||||
|
||||
// ========================= FieldNames =========================
|
||||
const mergedFieldNames = React.useMemo(() => (0, _valueUtil.fillFieldNames)(fieldNames, childrenAsData), /* eslint-disable react-hooks/exhaustive-deps */
|
||||
[
|
||||
// We stringify fieldNames to avoid unnecessary re-renders.
|
||||
JSON.stringify(fieldNames), childrenAsData]
|
||||
/* eslint-enable react-hooks/exhaustive-deps */);
|
||||
|
||||
// =========================== Search ===========================
|
||||
const [internalSearchValue, setSearchValue] = (0, _useControlledState.default)('', searchValue);
|
||||
const mergedSearchValue = internalSearchValue || '';
|
||||
|
||||
// =========================== Option ===========================
|
||||
const parsedOptions = (0, _useOptions.default)(options, children, mergedFieldNames, normalizedOptionFilterProp, optionLabelProp);
|
||||
const {
|
||||
valueOptions,
|
||||
labelOptions,
|
||||
options: mergedOptions
|
||||
} = parsedOptions;
|
||||
|
||||
// ========================= Wrap Value =========================
|
||||
const convert2LabelValues = React.useCallback(draftValues => {
|
||||
// Convert to array
|
||||
const valueList = (0, _commonUtil.toArray)(draftValues);
|
||||
|
||||
// Convert to labelInValue type
|
||||
return valueList.map(val => {
|
||||
let rawValue;
|
||||
let rawLabel;
|
||||
let rawDisabled;
|
||||
let rawTitle;
|
||||
|
||||
// Fill label & value
|
||||
if (isRawValue(val)) {
|
||||
rawValue = val;
|
||||
} else {
|
||||
rawLabel = val.label;
|
||||
rawValue = val.value;
|
||||
}
|
||||
const option = valueOptions.get(rawValue);
|
||||
if (option) {
|
||||
// Fill missing props
|
||||
if (rawLabel === undefined) rawLabel = option?.[optionLabelProp || mergedFieldNames.label];
|
||||
rawDisabled = option?.disabled;
|
||||
rawTitle = option?.title;
|
||||
|
||||
// Warning if label not same as provided
|
||||
if (process.env.NODE_ENV !== 'production' && !optionLabelProp) {
|
||||
const optionLabel = option?.[mergedFieldNames.label];
|
||||
if (optionLabel !== undefined && ! /*#__PURE__*/React.isValidElement(optionLabel) && ! /*#__PURE__*/React.isValidElement(rawLabel) && optionLabel !== rawLabel) {
|
||||
(0, _warning.default)(false, '`label` of `value` is not same as `label` in Select options.');
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: rawLabel,
|
||||
value: rawValue,
|
||||
key: rawValue,
|
||||
disabled: rawDisabled,
|
||||
title: rawTitle
|
||||
};
|
||||
});
|
||||
}, [mergedFieldNames, optionLabelProp, valueOptions]);
|
||||
|
||||
// =========================== Values ===========================
|
||||
const [internalValue, setInternalValue] = (0, _useControlledState.default)(defaultValue, value);
|
||||
|
||||
// Merged value with LabelValueType
|
||||
const rawLabeledValues = React.useMemo(() => {
|
||||
const newInternalValue = multiple && internalValue === null ? [] : internalValue;
|
||||
const values = convert2LabelValues(newInternalValue);
|
||||
|
||||
// combobox no need save value when it's no value (exclude value equal 0)
|
||||
if (mode === 'combobox' && (0, _commonUtil.isComboNoValue)(values[0]?.value)) {
|
||||
return [];
|
||||
}
|
||||
return values;
|
||||
}, [internalValue, convert2LabelValues, mode, multiple]);
|
||||
|
||||
// Fill label with cache to avoid option remove
|
||||
const [mergedValues, getMixedOption] = (0, _useCache.default)(rawLabeledValues, valueOptions);
|
||||
const displayValues = React.useMemo(() => {
|
||||
// `null` need show as placeholder instead
|
||||
// https://github.com/ant-design/ant-design/issues/25057
|
||||
if (!mode && mergedValues.length === 1) {
|
||||
const firstValue = mergedValues[0];
|
||||
if (firstValue.value === null && (firstValue.label === null || firstValue.label === undefined)) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return mergedValues.map(item => ({
|
||||
...item,
|
||||
label: (typeof labelRender === 'function' ? labelRender(item) : item.label) ?? item.value
|
||||
}));
|
||||
}, [mode, mergedValues, labelRender]);
|
||||
|
||||
/** Convert `displayValues` to raw value type set */
|
||||
const rawValues = React.useMemo(() => new Set(mergedValues.map(val => val.value)), [mergedValues]);
|
||||
React.useEffect(() => {
|
||||
if (mode === 'combobox') {
|
||||
const strValue = mergedValues[0]?.value;
|
||||
setSearchValue((0, _commonUtil.hasValue)(strValue) ? String(strValue) : '');
|
||||
}
|
||||
}, [mergedValues]);
|
||||
|
||||
// ======================= Display Option =======================
|
||||
// Create a placeholder item if not exist in `options`
|
||||
const createTagOption = (0, _useRefFunc.default)((val, label) => {
|
||||
const mergedLabel = label ?? val;
|
||||
return {
|
||||
[mergedFieldNames.value]: val,
|
||||
[mergedFieldNames.label]: mergedLabel
|
||||
};
|
||||
});
|
||||
|
||||
// Fill tag as option if mode is `tags`
|
||||
const filledTagOptions = React.useMemo(() => {
|
||||
if (mode !== 'tags') {
|
||||
return mergedOptions;
|
||||
}
|
||||
|
||||
// >>> Tag mode
|
||||
const cloneOptions = [...mergedOptions];
|
||||
|
||||
// Check if value exist in options (include new patch item)
|
||||
const existOptions = val => valueOptions.has(val);
|
||||
|
||||
// Fill current value as option
|
||||
[...mergedValues].sort((a, b) => a.value < b.value ? -1 : 1).forEach(item => {
|
||||
const val = item.value;
|
||||
if (!existOptions(val)) {
|
||||
cloneOptions.push(createTagOption(val, item.label));
|
||||
}
|
||||
});
|
||||
return cloneOptions;
|
||||
}, [createTagOption, mergedOptions, valueOptions, mergedValues, mode]);
|
||||
const filteredOptions = (0, _useFilterOptions.default)(filledTagOptions, mergedFieldNames, mergedSearchValue, mergedFilterOption, normalizedOptionFilterProp);
|
||||
|
||||
// Fill options with search value if needed
|
||||
const filledSearchOptions = React.useMemo(() => {
|
||||
const hasItemMatchingSearch = item => {
|
||||
if (normalizedOptionFilterProp.length) {
|
||||
return normalizedOptionFilterProp.some(prop => item?.[prop] === mergedSearchValue);
|
||||
}
|
||||
return item?.value === mergedSearchValue;
|
||||
};
|
||||
if (mode !== 'tags' || !mergedSearchValue || filteredOptions.some(item => hasItemMatchingSearch(item))) {
|
||||
return filteredOptions;
|
||||
}
|
||||
// ignore when search value equal select input value
|
||||
if (filteredOptions.some(item => item[mergedFieldNames.value] === mergedSearchValue)) {
|
||||
return filteredOptions;
|
||||
}
|
||||
// Fill search value as option
|
||||
return [createTagOption(mergedSearchValue), ...filteredOptions];
|
||||
}, [createTagOption, normalizedOptionFilterProp, mode, filteredOptions, mergedSearchValue, mergedFieldNames]);
|
||||
const sorter = inputOptions => {
|
||||
const sortedOptions = [...inputOptions].sort((a, b) => filterSort(a, b, {
|
||||
searchValue: mergedSearchValue
|
||||
}));
|
||||
return sortedOptions.map(item => {
|
||||
if (Array.isArray(item.options)) {
|
||||
return {
|
||||
...item,
|
||||
options: item.options.length > 0 ? sorter(item.options) : item.options
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
};
|
||||
const orderedFilteredOptions = React.useMemo(() => {
|
||||
if (!filterSort) {
|
||||
return filledSearchOptions;
|
||||
}
|
||||
return sorter(filledSearchOptions);
|
||||
}, [filledSearchOptions, filterSort, mergedSearchValue]);
|
||||
const displayOptions = React.useMemo(() => (0, _valueUtil.flattenOptions)(orderedFilteredOptions, {
|
||||
fieldNames: mergedFieldNames,
|
||||
childrenAsData
|
||||
}), [orderedFilteredOptions, mergedFieldNames, childrenAsData]);
|
||||
|
||||
// =========================== Change ===========================
|
||||
const triggerChange = values => {
|
||||
const labeledValues = convert2LabelValues(values);
|
||||
setInternalValue(labeledValues);
|
||||
if (onChange && (
|
||||
// Trigger event only when value changed
|
||||
labeledValues.length !== mergedValues.length || labeledValues.some((newVal, index) => mergedValues[index]?.value !== newVal?.value))) {
|
||||
const returnValues = labelInValue ? labeledValues.map(({
|
||||
label: l,
|
||||
value: v
|
||||
}) => ({
|
||||
label: l,
|
||||
value: v
|
||||
})) : labeledValues.map(v => v.value);
|
||||
const returnOptions = labeledValues.map(v => (0, _valueUtil.injectPropsWithOption)(getMixedOption(v.value)));
|
||||
onChange(
|
||||
// Value
|
||||
multiple ? returnValues : returnValues[0],
|
||||
// Option
|
||||
multiple ? returnOptions : returnOptions[0]);
|
||||
}
|
||||
};
|
||||
|
||||
// ======================= Accessibility ========================
|
||||
const [activeValue, setActiveValue] = React.useState(null);
|
||||
const [accessibilityIndex, setAccessibilityIndex] = React.useState(0);
|
||||
const mergedDefaultActiveFirstOption = defaultActiveFirstOption !== undefined ? defaultActiveFirstOption : mode !== 'combobox';
|
||||
const activeEventRef = React.useRef();
|
||||
const onActiveValue = React.useCallback((active, index, {
|
||||
source = 'keyboard'
|
||||
} = {}) => {
|
||||
setAccessibilityIndex(index);
|
||||
if (backfill && mode === 'combobox' && active !== null && source === 'keyboard') {
|
||||
setActiveValue(String(active));
|
||||
}
|
||||
|
||||
// Active will call multiple times.
|
||||
// We only need trigger the last one.
|
||||
const promise = Promise.resolve().then(() => {
|
||||
if (activeEventRef.current === promise) {
|
||||
onActive?.(active);
|
||||
}
|
||||
});
|
||||
activeEventRef.current = promise;
|
||||
}, [backfill, mode, onActive]);
|
||||
|
||||
// ========================= OptionList =========================
|
||||
const triggerSelect = (val, selected, type) => {
|
||||
const getSelectEnt = () => {
|
||||
const option = getMixedOption(val);
|
||||
return [labelInValue ? {
|
||||
label: option?.[mergedFieldNames.label],
|
||||
value: val
|
||||
} : val, (0, _valueUtil.injectPropsWithOption)(option)];
|
||||
};
|
||||
if (selected && onSelect) {
|
||||
const [wrappedValue, option] = getSelectEnt();
|
||||
onSelect(wrappedValue, option);
|
||||
} else if (!selected && onDeselect && type !== 'clear') {
|
||||
const [wrappedValue, option] = getSelectEnt();
|
||||
onDeselect(wrappedValue, option);
|
||||
}
|
||||
};
|
||||
|
||||
// Used for OptionList selection
|
||||
const onInternalSelect = (0, _useRefFunc.default)((val, info) => {
|
||||
let cloneValues;
|
||||
|
||||
// Single mode always trigger select only with option list
|
||||
const mergedSelect = multiple ? info.selected : true;
|
||||
if (mergedSelect) {
|
||||
cloneValues = multiple ? [...mergedValues, val] : [val];
|
||||
} else {
|
||||
cloneValues = mergedValues.filter(v => v.value !== val);
|
||||
}
|
||||
triggerChange(cloneValues);
|
||||
triggerSelect(val, mergedSelect);
|
||||
|
||||
// Clean search value if single or configured
|
||||
if (mode === 'combobox') {
|
||||
setActiveValue('');
|
||||
} else if (!_BaseSelect.isMultiple || autoClearSearchValue) {
|
||||
setSearchValue('');
|
||||
setActiveValue('');
|
||||
}
|
||||
});
|
||||
|
||||
// ======================= Display Change =======================
|
||||
// BaseSelect display values change
|
||||
const onDisplayValuesChange = (nextValues, info) => {
|
||||
triggerChange(nextValues);
|
||||
const {
|
||||
type,
|
||||
values
|
||||
} = info;
|
||||
if (type === 'remove' || type === 'clear') {
|
||||
values.forEach(item => {
|
||||
triggerSelect(item.value, false, type);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// =========================== Search ===========================
|
||||
const onInternalSearch = (searchText, info) => {
|
||||
setSearchValue(searchText);
|
||||
setActiveValue(null);
|
||||
|
||||
// [Submit] Tag mode should flush input
|
||||
if (info.source === 'submit') {
|
||||
const formatted = (searchText || '').trim();
|
||||
// prevent empty tags from appearing when you click the Enter button
|
||||
if (formatted) {
|
||||
const newRawValues = Array.from(new Set([...rawValues, formatted]));
|
||||
triggerChange(newRawValues);
|
||||
triggerSelect(formatted, true);
|
||||
setSearchValue('');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (info.source !== 'blur') {
|
||||
if (mode === 'combobox') {
|
||||
triggerChange(searchText);
|
||||
}
|
||||
onSearch?.(searchText);
|
||||
}
|
||||
};
|
||||
const onInternalSearchSplit = words => {
|
||||
let patchValues = words;
|
||||
if (mode !== 'tags') {
|
||||
patchValues = words.map(word => {
|
||||
const opt = labelOptions.get(word);
|
||||
return opt?.value;
|
||||
}).filter(val => val !== undefined);
|
||||
}
|
||||
const newRawValues = Array.from(new Set([...rawValues, ...patchValues]));
|
||||
triggerChange(newRawValues);
|
||||
newRawValues.forEach(newRawValue => {
|
||||
triggerSelect(newRawValue, true);
|
||||
});
|
||||
};
|
||||
|
||||
// ========================== Context ===========================
|
||||
const selectContext = React.useMemo(() => {
|
||||
const realVirtual = virtual !== false && popupMatchSelectWidth !== false;
|
||||
return {
|
||||
...parsedOptions,
|
||||
flattenOptions: displayOptions,
|
||||
onActiveValue,
|
||||
defaultActiveFirstOption: mergedDefaultActiveFirstOption,
|
||||
onSelect: onInternalSelect,
|
||||
menuItemSelectedIcon,
|
||||
rawValues,
|
||||
fieldNames: mergedFieldNames,
|
||||
virtual: realVirtual,
|
||||
direction,
|
||||
listHeight,
|
||||
listItemHeight,
|
||||
childrenAsData,
|
||||
maxCount,
|
||||
optionRender,
|
||||
classNames,
|
||||
styles
|
||||
};
|
||||
}, [maxCount, parsedOptions, displayOptions, onActiveValue, mergedDefaultActiveFirstOption, onInternalSelect, menuItemSelectedIcon, rawValues, mergedFieldNames, virtual, popupMatchSelectWidth, direction, listHeight, listItemHeight, childrenAsData, optionRender, classNames, styles]);
|
||||
|
||||
// ========================== Warning ===========================
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
(0, _warningPropsUtil.default)(props);
|
||||
(0, _warningPropsUtil.warningNullOptions)(mergedOptions, mergedFieldNames);
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
// == Render ==
|
||||
// ==============================================================
|
||||
return /*#__PURE__*/React.createElement(_SelectContext.default.Provider, {
|
||||
value: selectContext
|
||||
}, /*#__PURE__*/React.createElement(_BaseSelect.default, _extends({}, restProps, {
|
||||
// >>> MISC
|
||||
id: mergedId,
|
||||
prefixCls: prefixCls,
|
||||
ref: ref,
|
||||
omitDomProps: OMIT_DOM_PROPS,
|
||||
mode: mode
|
||||
// >>> Style
|
||||
,
|
||||
classNames: classNames,
|
||||
styles: styles
|
||||
// >>> Values
|
||||
,
|
||||
displayValues: displayValues,
|
||||
onDisplayValuesChange: onDisplayValuesChange,
|
||||
maxCount: maxCount
|
||||
// >>> Trigger
|
||||
,
|
||||
direction: direction
|
||||
// >>> Search
|
||||
,
|
||||
showSearch: mergedShowSearch,
|
||||
searchValue: mergedSearchValue,
|
||||
onSearch: onInternalSearch,
|
||||
autoClearSearchValue: autoClearSearchValue,
|
||||
onSearchSplit: onInternalSearchSplit,
|
||||
popupMatchSelectWidth: popupMatchSelectWidth
|
||||
// >>> OptionList
|
||||
,
|
||||
OptionList: _OptionList.default,
|
||||
emptyOptions: !displayOptions.length
|
||||
// >>> Accessibility
|
||||
,
|
||||
activeValue: activeValue,
|
||||
activeDescendantId: `${mergedId}_list_${accessibilityIndex}`
|
||||
})));
|
||||
});
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Select.displayName = 'Select';
|
||||
}
|
||||
const TypedSelect = Select;
|
||||
TypedSelect.Option = _Option.default;
|
||||
TypedSelect.OptGroup = _OptGroup.default;
|
||||
var _default = exports.default = TypedSelect;
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import * as React from 'react';
|
||||
import type { RawValueType, RenderNode } from './BaseSelect';
|
||||
import type { BaseOptionType, FieldNames, OnActiveValue, OnInternalSelect, SelectProps, SemanticName, PopupSemantic } from './Select';
|
||||
import type { FlattenOptionData } from './interface';
|
||||
/**
|
||||
* SelectContext is only used for Select. BaseSelect should not consume this context.
|
||||
*/
|
||||
export interface SelectContextProps {
|
||||
classNames?: Partial<Record<SemanticName, string>> & {
|
||||
popup?: Partial<Record<PopupSemantic, string>>;
|
||||
};
|
||||
styles?: Partial<Record<SemanticName, React.CSSProperties>> & {
|
||||
popup?: Partial<Record<PopupSemantic, React.CSSProperties>>;
|
||||
};
|
||||
options: BaseOptionType[];
|
||||
optionRender?: SelectProps['optionRender'];
|
||||
flattenOptions: FlattenOptionData<BaseOptionType>[];
|
||||
onActiveValue: OnActiveValue;
|
||||
defaultActiveFirstOption?: boolean;
|
||||
onSelect: OnInternalSelect;
|
||||
menuItemSelectedIcon?: RenderNode;
|
||||
rawValues: Set<RawValueType>;
|
||||
fieldNames?: FieldNames;
|
||||
virtual?: boolean;
|
||||
direction?: 'ltr' | 'rtl';
|
||||
listHeight?: number;
|
||||
listItemHeight?: number;
|
||||
childrenAsData?: boolean;
|
||||
maxCount?: number;
|
||||
}
|
||||
declare const SelectContext: React.Context<SelectContextProps>;
|
||||
export default SelectContext;
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.default = void 0;
|
||||
var React = _interopRequireWildcard(require("react"));
|
||||
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
||||
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
||||
// Use any here since we do not get the type during compilation
|
||||
/**
|
||||
* SelectContext is only used for Select. BaseSelect should not consume this context.
|
||||
*/
|
||||
|
||||
const SelectContext = /*#__PURE__*/React.createContext(null);
|
||||
var _default = exports.default = SelectContext;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import * as React from 'react';
|
||||
export interface AffixProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
export default function Affix(props: AffixProps): React.JSX.Element;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.default = Affix;
|
||||
var React = _interopRequireWildcard(require("react"));
|
||||
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
||||
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
||||
// Affix is a simple wrapper which should not read context or logical props
|
||||
function Affix(props) {
|
||||
const {
|
||||
children,
|
||||
...restProps
|
||||
} = props;
|
||||
if (!children) {
|
||||
return null;
|
||||
}
|
||||
return /*#__PURE__*/React.createElement("div", restProps, children);
|
||||
}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import type { SharedContentProps } from '.';
|
||||
declare const _default: React.ForwardRefExoticComponent<SharedContentProps & React.RefAttributes<HTMLInputElement>>;
|
||||
export default _default;
|
||||
Generated
Vendored
+167
@@ -0,0 +1,167 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.default = void 0;
|
||||
var React = _interopRequireWildcard(require("react"));
|
||||
var _clsx = require("clsx");
|
||||
var _overflow = _interopRequireDefault(require("@rc-component/overflow"));
|
||||
var _Input = _interopRequireDefault(require("../Input"));
|
||||
var _context = require("../context");
|
||||
var _TransBtn = _interopRequireDefault(require("../../TransBtn"));
|
||||
var _commonUtil = require("../../utils/commonUtil");
|
||||
var _useBaseProps = _interopRequireDefault(require("../../hooks/useBaseProps"));
|
||||
var _Placeholder = _interopRequireDefault(require("./Placeholder"));
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
||||
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
||||
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
||||
function itemKey(value) {
|
||||
return value.key ?? value.value;
|
||||
}
|
||||
const onPreventMouseDown = event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
var _default = exports.default = /*#__PURE__*/React.forwardRef(function MultipleContent({
|
||||
inputProps
|
||||
}, ref) {
|
||||
const {
|
||||
prefixCls,
|
||||
displayValues,
|
||||
searchValue,
|
||||
mode,
|
||||
onSelectorRemove,
|
||||
removeIcon: removeIconFromContext
|
||||
} = (0, _context.useSelectInputContext)();
|
||||
const {
|
||||
disabled,
|
||||
showSearch,
|
||||
triggerOpen,
|
||||
rawOpen,
|
||||
toggleOpen,
|
||||
autoClearSearchValue,
|
||||
tagRender: tagRenderFromContext,
|
||||
maxTagPlaceholder: maxTagPlaceholderFromContext,
|
||||
maxTagTextLength,
|
||||
maxTagCount,
|
||||
classNames,
|
||||
styles
|
||||
} = (0, _useBaseProps.default)();
|
||||
const selectionItemPrefixCls = `${prefixCls}-selection-item`;
|
||||
|
||||
// ===================== Search ======================
|
||||
// Apply autoClearSearchValue logic: when dropdown is closed and autoClearSearchValue is not false (default true), clear search value
|
||||
// Use rawOpen to avoid clearing search when emptyListContent blocks open
|
||||
let computedSearchValue = searchValue;
|
||||
if (!rawOpen && mode === 'multiple' && autoClearSearchValue !== false) {
|
||||
computedSearchValue = '';
|
||||
}
|
||||
const inputValue = showSearch ? computedSearchValue || '' : '';
|
||||
const inputEditable = showSearch && !disabled;
|
||||
|
||||
// Props from context with safe defaults
|
||||
const removeIcon = removeIconFromContext ?? '×';
|
||||
const maxTagPlaceholder = maxTagPlaceholderFromContext ?? (omittedValues => `+ ${omittedValues.length} ...`);
|
||||
const tagRender = tagRenderFromContext;
|
||||
const onToggleOpen = newOpen => {
|
||||
toggleOpen(newOpen);
|
||||
};
|
||||
const onRemove = value => {
|
||||
onSelectorRemove?.(value);
|
||||
};
|
||||
|
||||
// ======================== Item ========================
|
||||
// >>> Render Selector Node. Includes Item & Rest
|
||||
const defaultRenderSelector = (item, content, itemDisabled, closable, onClose) => /*#__PURE__*/React.createElement("span", {
|
||||
title: (0, _commonUtil.getTitle)(item),
|
||||
className: (0, _clsx.clsx)(selectionItemPrefixCls, {
|
||||
[`${selectionItemPrefixCls}-disabled`]: itemDisabled
|
||||
}, classNames?.item),
|
||||
style: styles?.item
|
||||
}, /*#__PURE__*/React.createElement("span", {
|
||||
className: (0, _clsx.clsx)(`${selectionItemPrefixCls}-content`, classNames?.itemContent),
|
||||
style: styles?.itemContent
|
||||
}, content), closable && /*#__PURE__*/React.createElement(_TransBtn.default, {
|
||||
className: (0, _clsx.clsx)(`${selectionItemPrefixCls}-remove`, classNames?.itemRemove),
|
||||
style: styles?.itemRemove,
|
||||
onMouseDown: onPreventMouseDown,
|
||||
onClick: onClose,
|
||||
customizeIcon: removeIcon
|
||||
}, "\xD7"));
|
||||
const customizeRenderSelector = (value, content, itemDisabled, closable, onClose, isMaxTag, info) => {
|
||||
const onMouseDown = e => {
|
||||
onPreventMouseDown(e);
|
||||
onToggleOpen(!triggerOpen);
|
||||
};
|
||||
return /*#__PURE__*/React.createElement("span", {
|
||||
onMouseDown: onMouseDown
|
||||
}, tagRender({
|
||||
label: content,
|
||||
value,
|
||||
index: info?.index,
|
||||
disabled: itemDisabled,
|
||||
closable,
|
||||
onClose,
|
||||
isMaxTag: !!isMaxTag
|
||||
}));
|
||||
};
|
||||
|
||||
// ====================== Overflow ======================
|
||||
const renderItem = (valueItem, info) => {
|
||||
const {
|
||||
disabled: itemDisabled,
|
||||
label,
|
||||
value
|
||||
} = valueItem;
|
||||
const closable = !disabled && !itemDisabled;
|
||||
let displayLabel = label;
|
||||
if (typeof maxTagTextLength === 'number') {
|
||||
if (typeof label === 'string' || typeof label === 'number') {
|
||||
const strLabel = String(displayLabel);
|
||||
if (strLabel.length > maxTagTextLength) {
|
||||
displayLabel = `${strLabel.slice(0, maxTagTextLength)}...`;
|
||||
}
|
||||
}
|
||||
}
|
||||
const onClose = event => {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
onRemove(valueItem);
|
||||
};
|
||||
return typeof tagRender === 'function' ? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose, undefined, info) : defaultRenderSelector(valueItem, displayLabel, itemDisabled, closable, onClose);
|
||||
};
|
||||
const renderRest = omittedValues => {
|
||||
// https://github.com/ant-design/ant-design/issues/48930
|
||||
if (!displayValues.length) {
|
||||
return null;
|
||||
}
|
||||
const content = typeof maxTagPlaceholder === 'function' ? maxTagPlaceholder(omittedValues) : maxTagPlaceholder;
|
||||
return typeof tagRender === 'function' ? customizeRenderSelector(undefined, content, false, false, undefined, true) : defaultRenderSelector({
|
||||
title: content
|
||||
}, content, false);
|
||||
};
|
||||
|
||||
// ======================= Render =======================
|
||||
return /*#__PURE__*/React.createElement(_overflow.default, {
|
||||
prefixCls: `${prefixCls}-content`,
|
||||
className: classNames?.content,
|
||||
style: styles?.content,
|
||||
prefix: !displayValues.length && !inputValue && /*#__PURE__*/React.createElement(_Placeholder.default, null),
|
||||
data: displayValues,
|
||||
renderItem: renderItem,
|
||||
renderRest: renderRest,
|
||||
suffix: /*#__PURE__*/React.createElement(_Input.default, _extends({
|
||||
ref: ref,
|
||||
disabled: disabled,
|
||||
readOnly: !inputEditable
|
||||
}, inputProps, {
|
||||
value: inputValue || '',
|
||||
syncWidth: true
|
||||
})),
|
||||
itemKey: itemKey,
|
||||
maxCount: maxTagCount
|
||||
});
|
||||
});
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import * as React from 'react';
|
||||
export interface PlaceholderProps {
|
||||
show?: boolean;
|
||||
}
|
||||
export default function Placeholder(props: PlaceholderProps): React.JSX.Element;
|
||||
Generated
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.default = Placeholder;
|
||||
var React = _interopRequireWildcard(require("react"));
|
||||
var _clsx = require("clsx");
|
||||
var _context = require("../context");
|
||||
var _useBaseProps = _interopRequireDefault(require("../../hooks/useBaseProps"));
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
||||
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
||||
function Placeholder(props) {
|
||||
const {
|
||||
prefixCls,
|
||||
placeholder,
|
||||
displayValues
|
||||
} = (0, _context.useSelectInputContext)();
|
||||
const {
|
||||
classNames,
|
||||
styles
|
||||
} = (0, _useBaseProps.default)();
|
||||
const {
|
||||
show = true
|
||||
} = props;
|
||||
if (displayValues.length) {
|
||||
return null;
|
||||
}
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
className: (0, _clsx.clsx)(`${prefixCls}-placeholder`, classNames?.placeholder),
|
||||
style: {
|
||||
visibility: show ? 'visible' : 'hidden',
|
||||
...styles?.placeholder
|
||||
}
|
||||
}, placeholder);
|
||||
}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import type { SharedContentProps } from '.';
|
||||
declare const SingleContent: React.ForwardRefExoticComponent<SharedContentProps & React.RefAttributes<HTMLInputElement>>;
|
||||
export default SingleContent;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user