mirror of
https://github.com/apache/superset.git
synced 2024-09-12 08:39:45 -04:00
chore: Improves the Select component UI/UX - iteration 2 (#15235)
This commit is contained in:
parent
5b2eb8d933
commit
fc1a62b78c
@ -1,244 +0,0 @@
|
|||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import React from 'react';
|
|
||||||
import Select, { OptionsType, SelectProps } from './AntdSelect';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'Select',
|
|
||||||
component: Select,
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = [
|
|
||||||
{
|
|
||||||
label: 'Such an incredibly awesome long long label',
|
|
||||||
value: 'Such an incredibly awesome long long label',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Another incredibly awesome long long label',
|
|
||||||
value: 'Another incredibly awesome long long label',
|
|
||||||
},
|
|
||||||
{ label: 'Just a label', value: 'Just a label' },
|
|
||||||
{ label: 'A', value: 'A' },
|
|
||||||
{ label: 'B', value: 'B' },
|
|
||||||
{ label: 'C', value: 'C' },
|
|
||||||
{ label: 'D', value: 'D' },
|
|
||||||
{ label: 'E', value: 'E' },
|
|
||||||
{ label: 'F', value: 'F' },
|
|
||||||
{ label: 'G', value: 'G' },
|
|
||||||
{ label: 'H', value: 'H' },
|
|
||||||
{ label: 'I', value: 'I' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const selectPositions = [
|
|
||||||
{
|
|
||||||
id: 'topLeft',
|
|
||||||
style: { top: '0', left: '0' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'topRight',
|
|
||||||
style: { top: '0', right: '0' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'bottomLeft',
|
|
||||||
style: { bottom: '0', left: '0' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'bottomRight',
|
|
||||||
style: { bottom: '0', right: '0' },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const AtEveryCorner = () => (
|
|
||||||
<>
|
|
||||||
{selectPositions.map(position => (
|
|
||||||
<div
|
|
||||||
key={position.id}
|
|
||||||
style={{
|
|
||||||
...position.style,
|
|
||||||
width: '120px',
|
|
||||||
position: 'absolute',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Select ariaLabel={`gallery-${position.id}`} options={options} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
AtEveryCorner.story = {
|
|
||||||
parameters: {
|
|
||||||
actions: {
|
|
||||||
disable: true,
|
|
||||||
},
|
|
||||||
controls: {
|
|
||||||
disable: true,
|
|
||||||
},
|
|
||||||
knobs: {
|
|
||||||
disable: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
async function fetchUserList(search: string, page = 0): Promise<OptionsType> {
|
|
||||||
const username = search.trim().toLowerCase();
|
|
||||||
return new Promise(resolve => {
|
|
||||||
const users = [
|
|
||||||
'John',
|
|
||||||
'Liam',
|
|
||||||
'Olivia',
|
|
||||||
'Emma',
|
|
||||||
'Noah',
|
|
||||||
'Ava',
|
|
||||||
'Oliver',
|
|
||||||
'Elijah',
|
|
||||||
'Charlotte',
|
|
||||||
'Diego',
|
|
||||||
'Evan',
|
|
||||||
'Michael',
|
|
||||||
'Giovanni',
|
|
||||||
'Luca',
|
|
||||||
'Paolo',
|
|
||||||
'Francesca',
|
|
||||||
'Chiara',
|
|
||||||
'Sara',
|
|
||||||
'Valentina',
|
|
||||||
'Jessica',
|
|
||||||
'Angelica',
|
|
||||||
'Mario',
|
|
||||||
'Marco',
|
|
||||||
'Andrea',
|
|
||||||
'Luigi',
|
|
||||||
'Quarto',
|
|
||||||
'Quinto',
|
|
||||||
'Sesto',
|
|
||||||
'Franco',
|
|
||||||
'Sandro',
|
|
||||||
'Alehandro',
|
|
||||||
'Johnny',
|
|
||||||
'Nikole',
|
|
||||||
'Igor',
|
|
||||||
'Sipatha',
|
|
||||||
'Thami',
|
|
||||||
'Munei',
|
|
||||||
'Guilherme',
|
|
||||||
'Umair',
|
|
||||||
'Ashfaq',
|
|
||||||
'Amna',
|
|
||||||
'Irfan',
|
|
||||||
'George',
|
|
||||||
'Naseer',
|
|
||||||
'Mohammad',
|
|
||||||
'Rick',
|
|
||||||
'Saliya',
|
|
||||||
'Claire',
|
|
||||||
'Benedetta',
|
|
||||||
'Ilenia',
|
|
||||||
];
|
|
||||||
|
|
||||||
let results: { label: string; value: string }[] = [];
|
|
||||||
|
|
||||||
if (!username) {
|
|
||||||
results = users.map(u => ({
|
|
||||||
label: u,
|
|
||||||
value: u,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
const foundUsers = users.find(u => u.toLowerCase().includes(username));
|
|
||||||
if (foundUsers && Array.isArray(foundUsers)) {
|
|
||||||
results = foundUsers.map(u => ({ label: u, value: u }));
|
|
||||||
}
|
|
||||||
if (foundUsers && typeof foundUsers === 'string') {
|
|
||||||
const u = foundUsers;
|
|
||||||
results = [{ label: u, value: u }];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const offset = !page ? 0 : page * 10;
|
|
||||||
const resultsNum = !page ? 10 : (page + 1) * 10;
|
|
||||||
results = results.length ? results.splice(offset, resultsNum) : [];
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn(
|
|
||||||
`Emulating network request for search string: ${
|
|
||||||
username || '"empty"'
|
|
||||||
} and page: ${page} with results: [${results
|
|
||||||
.map(u => u.value)
|
|
||||||
.join(', ')}]`,
|
|
||||||
);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve(results);
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchUserListError(): Promise<OptionsType> {
|
|
||||||
return new Promise((_, reject) => {
|
|
||||||
// eslint-disable-next-line prefer-promise-reject-errors
|
|
||||||
reject('This is an error');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AsyncSelect = (args: SelectProps & { withError: boolean }) => (
|
|
||||||
<Select
|
|
||||||
{...args}
|
|
||||||
options={args.withError ? fetchUserListError : fetchUserList}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
AsyncSelect.args = {
|
|
||||||
withError: false,
|
|
||||||
allowNewOptions: false,
|
|
||||||
paginatedFetch: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
AsyncSelect.argTypes = {
|
|
||||||
mode: {
|
|
||||||
control: { type: 'select', options: ['single', 'multiple', 'tags'] },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
AsyncSelect.story = {
|
|
||||||
parameters: {
|
|
||||||
knobs: {
|
|
||||||
disable: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const InteractiveSelect = (args: SelectProps) => <Select {...args} />;
|
|
||||||
|
|
||||||
InteractiveSelect.args = {
|
|
||||||
allowNewOptions: false,
|
|
||||||
options,
|
|
||||||
showSearch: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
InteractiveSelect.argTypes = {
|
|
||||||
mode: {
|
|
||||||
control: { type: 'select', options: ['single', 'multiple', 'tags'] },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
InteractiveSelect.story = {
|
|
||||||
parameters: {
|
|
||||||
knobs: {
|
|
||||||
disable: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
313
superset-frontend/src/components/Select/Select.stories.tsx
Normal file
313
superset-frontend/src/components/Select/Select.stories.tsx
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React, { ReactNode, useState, useCallback } from 'react';
|
||||||
|
import Select, { SelectProps, OptionsPromiseResult } from './Select';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Select',
|
||||||
|
component: Select,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_WIDTH = 200;
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
label: 'Such an incredibly awesome long long label',
|
||||||
|
value: 'Such an incredibly awesome long long label',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Another incredibly awesome long long label',
|
||||||
|
value: 'Another incredibly awesome long long label',
|
||||||
|
},
|
||||||
|
{ label: 'Just a label', value: 'Just a label' },
|
||||||
|
{ label: 'A', value: 'A' },
|
||||||
|
{ label: 'B', value: 'B' },
|
||||||
|
{ label: 'C', value: 'C' },
|
||||||
|
{ label: 'D', value: 'D' },
|
||||||
|
{ label: 'E', value: 'E' },
|
||||||
|
{ label: 'F', value: 'F' },
|
||||||
|
{ label: 'G', value: 'G' },
|
||||||
|
{ label: 'H', value: 'H' },
|
||||||
|
{ label: 'I', value: 'I' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectPositions = [
|
||||||
|
{
|
||||||
|
id: 'topLeft',
|
||||||
|
style: { top: '0', left: '0' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'topRight',
|
||||||
|
style: { top: '0', right: '0' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bottomLeft',
|
||||||
|
style: { bottom: '0', left: '0' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bottomRight',
|
||||||
|
style: { bottom: '0', right: '0' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AtEveryCorner = () => (
|
||||||
|
<>
|
||||||
|
{selectPositions.map(position => (
|
||||||
|
<div
|
||||||
|
key={position.id}
|
||||||
|
style={{
|
||||||
|
...position.style,
|
||||||
|
width: DEFAULT_WIDTH,
|
||||||
|
position: 'absolute',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select ariaLabel={`gallery-${position.id}`} options={options} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<p style={{ position: 'absolute', top: '40%', left: '33%', width: 500 }}>
|
||||||
|
The objective of this panel is to show how the Select behaves when in
|
||||||
|
touch with the viewport extremities. In particular, how the drop-down is
|
||||||
|
displayed and if the tooltips of truncated items are correctly positioned.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
AtEveryCorner.story = {
|
||||||
|
parameters: {
|
||||||
|
actions: {
|
||||||
|
disable: true,
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
disable: true,
|
||||||
|
},
|
||||||
|
knobs: {
|
||||||
|
disable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const USERS = [
|
||||||
|
'John',
|
||||||
|
'Liam',
|
||||||
|
'Olivia',
|
||||||
|
'Emma',
|
||||||
|
'Noah',
|
||||||
|
'Ava',
|
||||||
|
'Oliver',
|
||||||
|
'Elijah',
|
||||||
|
'Charlotte',
|
||||||
|
'Diego',
|
||||||
|
'Evan',
|
||||||
|
'Michael',
|
||||||
|
'Giovanni',
|
||||||
|
'Luca',
|
||||||
|
'Paolo',
|
||||||
|
'Francesca',
|
||||||
|
'Chiara',
|
||||||
|
'Sara',
|
||||||
|
'Valentina',
|
||||||
|
'Jessica',
|
||||||
|
'Angelica',
|
||||||
|
'Mario',
|
||||||
|
'Marco',
|
||||||
|
'Andrea',
|
||||||
|
'Luigi',
|
||||||
|
'Quarto',
|
||||||
|
'Quinto',
|
||||||
|
'Sesto',
|
||||||
|
'Franco',
|
||||||
|
'Sandro',
|
||||||
|
'Alehandro',
|
||||||
|
'Johnny',
|
||||||
|
'Nikole',
|
||||||
|
'Igor',
|
||||||
|
'Sipatha',
|
||||||
|
'Thami',
|
||||||
|
'Munei',
|
||||||
|
'Guilherme',
|
||||||
|
'Umair',
|
||||||
|
'Ashfaq',
|
||||||
|
'Amna',
|
||||||
|
'Irfan',
|
||||||
|
'George',
|
||||||
|
'Naseer',
|
||||||
|
'Mohammad',
|
||||||
|
'Rick',
|
||||||
|
'Saliya',
|
||||||
|
'Claire',
|
||||||
|
'Benedetta',
|
||||||
|
'Ilenia',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AsyncSelect = (
|
||||||
|
args: SelectProps & { withError: boolean; responseTime: number },
|
||||||
|
) => {
|
||||||
|
const [requests, setRequests] = useState<ReactNode[]>([]);
|
||||||
|
|
||||||
|
const fetchUserList = useCallback(
|
||||||
|
(search: string, page = 0): Promise<OptionsPromiseResult> => {
|
||||||
|
const username = search.trim().toLowerCase();
|
||||||
|
return new Promise(resolve => {
|
||||||
|
let results: { label: string; value: string }[] = [];
|
||||||
|
|
||||||
|
if (!username) {
|
||||||
|
results = USERS.map(u => ({
|
||||||
|
label: u,
|
||||||
|
value: u,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
const foundUsers = USERS.find(u =>
|
||||||
|
u.toLowerCase().includes(username),
|
||||||
|
);
|
||||||
|
if (foundUsers && Array.isArray(foundUsers)) {
|
||||||
|
results = foundUsers.map(u => ({ label: u, value: u }));
|
||||||
|
}
|
||||||
|
if (foundUsers && typeof foundUsers === 'string') {
|
||||||
|
const u = foundUsers;
|
||||||
|
results = [{ label: u, value: u }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageSize = 10;
|
||||||
|
const offset = !page ? 0 : page * pageSize;
|
||||||
|
const resultsNum = !page ? pageSize : (page + 1) * pageSize;
|
||||||
|
results = results.length ? results.splice(offset, resultsNum) : [];
|
||||||
|
|
||||||
|
const request = (
|
||||||
|
<>
|
||||||
|
Emulating network request for page <b>{page}</b> and search{' '}
|
||||||
|
<b>{username || 'empty'}</b> ... <b>{resultsNum}</b> results
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
setRequests(requests => [request, ...requests]);
|
||||||
|
|
||||||
|
const totalPages =
|
||||||
|
USERS.length / pageSize + (USERS.length % pageSize > 0 ? 1 : 0);
|
||||||
|
|
||||||
|
const result: OptionsPromiseResult = {
|
||||||
|
data: results,
|
||||||
|
hasMoreData: page + 1 < totalPages,
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(result);
|
||||||
|
}, args.responseTime * 1000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[args.responseTime],
|
||||||
|
);
|
||||||
|
|
||||||
|
async function fetchUserListError(): Promise<OptionsPromiseResult> {
|
||||||
|
return new Promise((_, reject) => {
|
||||||
|
// eslint-disable-next-line prefer-promise-reject-errors
|
||||||
|
reject('This is an error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: DEFAULT_WIDTH,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
{...args}
|
||||||
|
options={args.withError ? fetchUserListError : fetchUserList}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 32,
|
||||||
|
left: DEFAULT_WIDTH + 100,
|
||||||
|
height: 400,
|
||||||
|
width: 600,
|
||||||
|
overflowY: 'auto',
|
||||||
|
border: '1px solid #d9d9d9',
|
||||||
|
padding: 20,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{requests.map(request => (
|
||||||
|
<p>{request}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AsyncSelect.args = {
|
||||||
|
withError: false,
|
||||||
|
allowNewOptions: false,
|
||||||
|
paginatedFetch: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
AsyncSelect.argTypes = {
|
||||||
|
mode: {
|
||||||
|
control: { type: 'select', options: ['single', 'multiple', 'tags'] },
|
||||||
|
},
|
||||||
|
responseTime: {
|
||||||
|
defaultValue: 1,
|
||||||
|
name: 'responseTime (seconds)',
|
||||||
|
control: {
|
||||||
|
type: 'range',
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
AsyncSelect.story = {
|
||||||
|
parameters: {
|
||||||
|
knobs: {
|
||||||
|
disable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InteractiveSelect = (args: SelectProps) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: DEFAULT_WIDTH,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select {...args} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
InteractiveSelect.args = {
|
||||||
|
allowNewOptions: false,
|
||||||
|
options,
|
||||||
|
showSearch: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
InteractiveSelect.argTypes = {
|
||||||
|
mode: {
|
||||||
|
control: { type: 'select', options: ['single', 'multiple', 'tags'] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
InteractiveSelect.story = {
|
||||||
|
parameters: {
|
||||||
|
knobs: {
|
||||||
|
disable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -39,6 +39,7 @@ import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
|||||||
import { hasOption } from './utils';
|
import { hasOption } from './utils';
|
||||||
|
|
||||||
type AntdSelectAllProps = AntdSelectProps<AntdSelectValue>;
|
type AntdSelectAllProps = AntdSelectProps<AntdSelectValue>;
|
||||||
|
|
||||||
type PickedSelectProps = Pick<
|
type PickedSelectProps = Pick<
|
||||||
AntdSelectAllProps,
|
AntdSelectAllProps,
|
||||||
| 'allowClear'
|
| 'allowClear'
|
||||||
@ -55,16 +56,25 @@ type PickedSelectProps = Pick<
|
|||||||
| 'showSearch'
|
| 'showSearch'
|
||||||
| 'value'
|
| 'value'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type OptionsType = Exclude<AntdSelectAllProps['options'], undefined>;
|
export type OptionsType = Exclude<AntdSelectAllProps['options'], undefined>;
|
||||||
|
|
||||||
|
export type OptionsPromiseResult = {
|
||||||
|
data: OptionsType;
|
||||||
|
hasMoreData: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type OptionsPromise = (
|
export type OptionsPromise = (
|
||||||
search: string,
|
search: string,
|
||||||
page?: number,
|
page?: number,
|
||||||
) => Promise<OptionsType>;
|
) => Promise<OptionsPromiseResult>;
|
||||||
|
|
||||||
export enum ESelectTypes {
|
export enum ESelectTypes {
|
||||||
MULTIPLE = 'multiple',
|
MULTIPLE = 'multiple',
|
||||||
TAGS = 'tags',
|
TAGS = 'tags',
|
||||||
SINGLE = '',
|
SINGLE = '',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectProps extends PickedSelectProps {
|
export interface SelectProps extends PickedSelectProps {
|
||||||
allowNewOptions?: boolean;
|
allowNewOptions?: boolean;
|
||||||
ariaLabel: string;
|
ariaLabel: string;
|
||||||
@ -75,10 +85,15 @@ export interface SelectProps extends PickedSelectProps {
|
|||||||
paginatedFetch?: boolean;
|
paginatedFetch?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
// unexposed default behaviors
|
// unexposed default behaviors
|
||||||
const MAX_TAG_COUNT = 4;
|
const MAX_TAG_COUNT = 4;
|
||||||
const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
|
const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
|
||||||
const DEBOUNCE_TIMEOUT = 800;
|
const DEBOUNCE_TIMEOUT = 500;
|
||||||
|
|
||||||
const Error = ({ error }: { error: string }) => {
|
const Error = ({ error }: { error: string }) => {
|
||||||
const StyledError = styled.div`
|
const StyledError = styled.div`
|
||||||
@ -108,7 +123,7 @@ const DropdownContent = ({
|
|||||||
return content;
|
return content;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SelectComponent = ({
|
const Select = ({
|
||||||
allowNewOptions = false,
|
allowNewOptions = false,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
filterOption,
|
filterOption,
|
||||||
@ -135,6 +150,7 @@ const SelectComponent = ({
|
|||||||
const [isLoading, setLoading] = useState(loading);
|
const [isLoading, setLoading] = useState(loading);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
|
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
|
||||||
|
const [hasMoreData, setHasMoreData] = useState(false);
|
||||||
const fetchRef = useRef(0);
|
const fetchRef = useRef(0);
|
||||||
|
|
||||||
const handleSelectMode = () => {
|
const handleSelectMode = () => {
|
||||||
@ -191,9 +207,8 @@ const SelectComponent = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFetch = useMemo(() => {
|
const handleFetch = useMemo(
|
||||||
const fetchOptions = options as OptionsPromise;
|
() => (value: string, paginate?: 'paginate') => {
|
||||||
const loadOptions = (value: string, paginate?: 'paginate') => {
|
|
||||||
if (paginate) {
|
if (paginate) {
|
||||||
fetchRef.current += 1;
|
fetchRef.current += 1;
|
||||||
} else {
|
} else {
|
||||||
@ -201,15 +216,17 @@ const SelectComponent = ({
|
|||||||
}
|
}
|
||||||
const fetchId = fetchRef.current;
|
const fetchId = fetchRef.current;
|
||||||
const page = paginatedFetch ? fetchId : undefined;
|
const page = paginatedFetch ? fetchId : undefined;
|
||||||
|
const fetchOptions = options as OptionsPromise;
|
||||||
fetchOptions(value, page)
|
fetchOptions(value, page)
|
||||||
.then((newOptions: OptionsType) => {
|
.then((result: OptionsPromiseResult) => {
|
||||||
|
const { data, hasMoreData } = result;
|
||||||
|
setHasMoreData(hasMoreData);
|
||||||
if (fetchId !== fetchRef.current) return;
|
if (fetchId !== fetchRef.current) return;
|
||||||
if (newOptions && Array.isArray(newOptions) && newOptions.length) {
|
if (data && Array.isArray(data) && data.length) {
|
||||||
// merges with existing and creates unique options
|
// merges with existing and creates unique options
|
||||||
setOptions(prevOptions => [
|
setOptions(prevOptions => [
|
||||||
...prevOptions,
|
...prevOptions,
|
||||||
...newOptions.filter(
|
...data.filter(
|
||||||
newOpt =>
|
newOpt =>
|
||||||
!prevOptions.find(prevOpt => prevOpt.value === newOpt.value),
|
!prevOptions.find(prevOpt => prevOpt.value === newOpt.value),
|
||||||
),
|
),
|
||||||
@ -223,11 +240,11 @@ const SelectComponent = ({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
},
|
||||||
return debounce(loadOptions, DEBOUNCE_TIMEOUT);
|
[options, paginatedFetch],
|
||||||
}, [options, paginatedFetch]);
|
);
|
||||||
|
|
||||||
const handleOnSearch = (search: string) => {
|
const handleOnSearch = debounce((search: string) => {
|
||||||
const searchValue = search.trim();
|
const searchValue = search.trim();
|
||||||
// enables option creation
|
// enables option creation
|
||||||
if (allowNewOptions && isSingleMode) {
|
if (allowNewOptions && isSingleMode) {
|
||||||
@ -252,11 +269,12 @@ const SelectComponent = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setSearchedValue(searchValue);
|
setSearchedValue(searchValue);
|
||||||
};
|
}, DEBOUNCE_TIMEOUT);
|
||||||
|
|
||||||
const handlePagination = (e: UIEvent<HTMLElement>) => {
|
const handlePagination = (e: UIEvent<HTMLElement>) => {
|
||||||
const vScroll = e.currentTarget;
|
const vScroll = e.currentTarget;
|
||||||
if (
|
if (
|
||||||
|
hasMoreData &&
|
||||||
isAsync &&
|
isAsync &&
|
||||||
paginatedFetch &&
|
paginatedFetch &&
|
||||||
vScroll.scrollTop === vScroll.scrollHeight - vScroll.offsetHeight
|
vScroll.scrollTop === vScroll.scrollHeight - vScroll.offsetHeight
|
||||||
@ -310,19 +328,21 @@ const SelectComponent = ({
|
|||||||
}
|
}
|
||||||
}, [allowNewOptions, isAsync, handleFetch, searchedValue]);
|
}, [allowNewOptions, isAsync, handleFetch, searchedValue]);
|
||||||
|
|
||||||
|
const dropdownRender = (
|
||||||
|
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
|
||||||
|
) => {
|
||||||
|
if (!isDropdownVisible) {
|
||||||
|
originNode.ref?.current?.scrollTo({ top: 0 });
|
||||||
|
}
|
||||||
|
return <DropdownContent content={originNode} error={error} />;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<StyledContainer>
|
||||||
{header}
|
{header}
|
||||||
<AntdSelect
|
<AntdSelect
|
||||||
aria-label={ariaLabel || name}
|
aria-label={ariaLabel || name}
|
||||||
dropdownRender={(
|
dropdownRender={dropdownRender}
|
||||||
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
|
|
||||||
) => {
|
|
||||||
if (!isDropdownVisible) {
|
|
||||||
originNode.ref?.current?.scrollTo({ top: 0 });
|
|
||||||
}
|
|
||||||
return <DropdownContent content={originNode} error={error} />;
|
|
||||||
}}
|
|
||||||
filterOption={handleFilterOption as any}
|
filterOption={handleFilterOption as any}
|
||||||
getPopupContainer={triggerNode => triggerNode.parentNode}
|
getPopupContainer={triggerNode => triggerNode.parentNode}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
@ -339,17 +359,11 @@ const SelectComponent = ({
|
|||||||
showSearch={shouldShowSearch}
|
showSearch={shouldShowSearch}
|
||||||
tokenSeparators={TOKEN_SEPARATORS}
|
tokenSeparators={TOKEN_SEPARATORS}
|
||||||
value={selectValue}
|
value={selectValue}
|
||||||
|
style={{ width: '100%' }}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Select = styled((
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
{ ...props }: SelectProps,
|
|
||||||
) => <SelectComponent {...props} />)`
|
|
||||||
width: 100%;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default Select;
|
export default Select;
|
@ -23,7 +23,7 @@ import {
|
|||||||
GroupedOptionsType,
|
GroupedOptionsType,
|
||||||
} from 'react-select';
|
} from 'react-select';
|
||||||
|
|
||||||
import { OptionsType as AntdOptionsType } from './AntdSelect';
|
import { OptionsType as AntdOptionsType } from './Select';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find Option value that matches a possibly string value.
|
* Find Option value that matches a possibly string value.
|
||||||
|
@ -17,4 +17,4 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { default as Select } from './Select/AntdSelect';
|
export { default as Select } from './Select/Select';
|
||||||
|
Loading…
Reference in New Issue
Block a user