This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
+9
View File
@@ -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
View File
@@ -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
[![@rc-component/select](https://nodei.co/npm/@rc-component/select.png)](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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
}
}
@@ -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;
@@ -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);
}
@@ -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;
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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);
}
@@ -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;
@@ -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
});
});
@@ -0,0 +1,5 @@
import * as React from 'react';
export interface PlaceholderProps {
show?: boolean;
}
export default function Placeholder(props: PlaceholderProps): React.JSX.Element;
@@ -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);
}
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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));
});
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
@@ -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;
@@ -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]);
};
@@ -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;
@@ -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);
}
@@ -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
View File
@@ -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];
});
@@ -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;
@@ -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]);
}
@@ -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;
@@ -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]);
});
@@ -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
View File
@@ -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];
}
@@ -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
View File
@@ -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];
}
@@ -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;
@@ -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;
@@ -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;
@@ -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;
}
@@ -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>];
@@ -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]);
}
@@ -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;
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -0,0 +1 @@
export {};
@@ -0,0 +1 @@
export declare function isPlatformMac(): boolean;
@@ -0,0 +1,3 @@
export function isPlatformMac() {
return true;
}
@@ -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;
@@ -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;
}
@@ -0,0 +1,2 @@
/** keyCode Judgment function */
export declare function isValidateOpenKey(currentKeyCode: number): boolean;
+20
View File
@@ -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)
);
}
@@ -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[];
@@ -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);
}
@@ -0,0 +1 @@
export declare function isPlatformMac(): boolean;
@@ -0,0 +1,4 @@
/* istanbul ignore file */
export function isPlatformMac() {
return /(mac\sos|macintosh)/i.test(navigator.appVersion);
}
@@ -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
View File
@@ -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;
}
};
@@ -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;
@@ -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;
@@ -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;
@@ -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);
}
@@ -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;
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
@@ -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
View File
@@ -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;
@@ -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;
@@ -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);
}
@@ -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;
@@ -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
});
});
@@ -0,0 +1,5 @@
import * as React from 'react';
export interface PlaceholderProps {
show?: boolean;
}
export default function Placeholder(props: PlaceholderProps): React.JSX.Element;
@@ -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);
}
@@ -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