Select: Overflow ellipsis and control over multi value wrapping (#76405)

* Select: Better overflow and wrapping behavior and control

* Update

* truncate

* minor update

* review fixes

* Remove legacy big
This commit is contained in:
Torkel Ödegaard 2023-11-14 08:29:12 +01:00 committed by GitHub
parent ea37a116f7
commit 867ff52b38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 75 additions and 68 deletions

View File

@ -860,8 +860,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"]
[0, 0, 0, "Unexpected any. Specify a different type.", "11"]
],
"packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@ -1,13 +1,11 @@
import React from 'react';
import { DropdownIndicatorProps } from 'react-select';
import { Icon } from '../Icon/Icon';
interface DropdownIndicatorProps {
isOpen: boolean;
}
export const DropdownIndicator = ({ isOpen }: DropdownIndicatorProps) => {
export function DropdownIndicator({ selectProps }: DropdownIndicatorProps) {
const isOpen = selectProps.menuIsOpen;
const icon = isOpen ? 'search' : 'angle-down';
const size = isOpen ? 'sm' : 'md';
return <Icon name={icon} size={size} />;
};
}

View File

@ -45,7 +45,6 @@ const meta: Meta = {
'renderControl',
'options',
'isOptionDisabled',
'maxVisibleValues',
'aria-label',
'noOptionsMessage',
'menuPosition',
@ -225,7 +224,7 @@ export const MultiSelectBasic: Story = (args) => {
const [value, setValue] = useState<Array<SelectableValue<string>>>([]);
return (
<>
<div style={{ maxWidth: '450px' }}>
<MultiSelect
options={generateOptions()}
value={value}
@ -236,13 +235,15 @@ export const MultiSelectBasic: Story = (args) => {
prefix={getPrefix(args.icon)}
{...args}
/>
</>
</div>
);
};
MultiSelectBasic.args = {
isClearable: false,
closeMenuOnSelect: false,
maxVisibleValues: 5,
noMultiValueWrap: false,
};
export const MultiSelectAsync: Story = (args) => {

View File

@ -1,6 +1,6 @@
import { t } from 'i18next';
import React, { ComponentProps, useCallback, useEffect, useRef, useState } from 'react';
import { default as ReactSelect } from 'react-select';
import { default as ReactSelect, IndicatorsContainerProps, Props as ReactSelectProps } from 'react-select';
import { default as ReactAsyncSelect } from 'react-select/async';
import { default as AsyncCreatable } from 'react-select/async-creatable';
import Creatable from 'react-select/creatable';
@ -25,31 +25,6 @@ import { useCustomSelectStyles } from './resetSelectStyles';
import { ActionMeta, InputActionMeta, SelectBaseProps } from './types';
import { cleanValue, findSelectedValue, omitDescriptions } from './utils';
interface ExtraValuesIndicatorProps {
maxVisibleValues?: number | undefined;
selectedValuesCount: number;
menuIsOpen: boolean;
showAllSelectedWhenOpen: boolean;
}
const renderExtraValuesIndicator = (props: ExtraValuesIndicatorProps) => {
const { maxVisibleValues, selectedValuesCount, menuIsOpen, showAllSelectedWhenOpen } = props;
if (
maxVisibleValues !== undefined &&
selectedValuesCount > maxVisibleValues &&
!(showAllSelectedWhenOpen && menuIsOpen)
) {
return (
<span key="excess-values" id="excess-values">
(+{selectedValuesCount - maxVisibleValues})
</span>
);
}
return null;
};
const CustomControl = (props: any) => {
const {
children,
@ -88,6 +63,12 @@ const CustomControl = (props: any) => {
);
};
interface SelectPropsWithExtras extends ReactSelectProps {
maxVisibleValues?: number | undefined;
showAllSelectedWhenOpen: boolean;
noMultiValueWrap?: boolean;
}
export function SelectBase<T, Rest = {}>({
allowCustomValue = false,
allowCreateWhileLoading = false,
@ -145,6 +126,7 @@ export function SelectBase<T, Rest = {}>({
tabSelectsValue = true,
value,
virtualized = false,
noMultiValueWrap,
width,
isValidNewOption,
formatOptionLabel,
@ -274,6 +256,7 @@ export function SelectBase<T, Rest = {}>({
showAllSelectedWhenOpen,
tabSelectsValue,
value: isMulti ? selectedValue : selectedValue?.[0],
noMultiValueWrap,
};
if (allowCustomValue) {
@ -305,31 +288,8 @@ export function SelectBase<T, Rest = {}>({
MenuList: SelectMenuComponent,
Group: SelectOptionGroup,
ValueContainer,
IndicatorsContainer(props: any) {
const { selectProps } = props;
const { value, showAllSelectedWhenOpen, maxVisibleValues, menuIsOpen } = selectProps;
if (maxVisibleValues !== undefined) {
const selectedValuesCount = value.length;
const indicatorChildren = [...props.children];
indicatorChildren.splice(
-1,
0,
renderExtraValuesIndicator({
maxVisibleValues,
selectedValuesCount,
showAllSelectedWhenOpen,
menuIsOpen,
})
);
return <IndicatorsContainer {...props}>{indicatorChildren}</IndicatorsContainer>;
}
return <IndicatorsContainer {...props} />;
},
IndicatorSeparator() {
return <></>;
},
IndicatorsContainer: CustomIndicatorsContainer,
IndicatorSeparator: IndicatorSeparator,
Control: CustomControl,
Option: SelectMenuOptions,
ClearIndicator(props: any) {
@ -361,9 +321,7 @@ export function SelectBase<T, Rest = {}>({
</div>
);
},
DropdownIndicator(props) {
return <DropdownIndicator isOpen={props.selectProps.menuIsOpen} />;
},
DropdownIndicator: DropdownIndicator,
SingleValue(props: any) {
return <SingleValue {...props} isDisabled={disabled} />;
},
@ -394,3 +352,37 @@ function defaultFormatCreateLabel(input: string) {
</div>
);
}
type CustomIndicatorsContainerProps = IndicatorsContainerProps & {
selectProps: SelectPropsWithExtras;
children: React.ReactNode;
};
function CustomIndicatorsContainer(props: CustomIndicatorsContainerProps) {
const { showAllSelectedWhenOpen, maxVisibleValues, menuIsOpen } = props.selectProps;
const value = props.getValue();
if (maxVisibleValues !== undefined && Array.isArray(props.children)) {
const selectedValuesCount = value.length;
if (selectedValuesCount > maxVisibleValues && !(showAllSelectedWhenOpen && menuIsOpen)) {
const indicatorChildren = [...props.children];
indicatorChildren.splice(
-1,
0,
<span key="excess-values" id="excess-values">
(+{selectedValuesCount - maxVisibleValues})
</span>
);
return <IndicatorsContainer {...props}>{indicatorChildren}</IndicatorsContainer>;
}
}
return <IndicatorsContainer {...props} />;
}
function IndicatorSeparator() {
return <></>;
}

View File

@ -30,8 +30,15 @@ class UnthemedValueContainer extends Component<any & { theme: GrafanaTheme2 }> {
renderContainer(children?: ReactNode) {
const { isMulti, theme } = this.props;
const noWrap = this.props.selectProps?.noMultiValueWrap && !this.props.selectProps?.menuIsOpen;
const styles = getSelectStyles(theme);
const className = cx(styles.valueContainer, isMulti && styles.valueContainerMulti);
const className = cx(
styles.valueContainer,
isMulti && styles.valueContainerMulti,
noWrap && styles.valueContainerMultiNoWrap
);
return <div className={className}>{children}</div>;
}
}

View File

@ -96,6 +96,9 @@ export const getSelectStyles = stylesFactory((theme: GrafanaTheme2) => {
flexWrap: 'wrap',
display: 'flex',
}),
valueContainerMultiNoWrap: css({
flexWrap: 'nowrap',
}),
loadingMessage: css({
label: 'grafana-select-loading-message',
padding: theme.spacing(1),
@ -113,6 +116,8 @@ export const getSelectStyles = stylesFactory((theme: GrafanaTheme2) => {
padding: theme.spacing(0.25, 0, 0.25, 1),
color: theme.colors.text.primary,
fontSize: theme.typography.size.sm,
overflow: 'hidden',
whiteSpace: 'nowrap',
'&:hover': {
background: theme.colors.emphasize(theme.colors.background.secondary),

View File

@ -24,6 +24,7 @@ export const generateOptions = (desc = false) => {
'Ok Vicente',
'Garry Spitz',
'Han Harnish',
'A very long value that is very long and takes up a lot of space and should be truncated preferrably if it does not fit',
];
return values.map<SelectableValue<string>>((name) => ({

View File

@ -30,7 +30,10 @@ export default function resetSelectStyles(theme: GrafanaTheme2) {
maxHeight,
}),
multiValue: () => ({}),
multiValueLabel: () => ({}),
multiValueLabel: () => ({
overflow: 'hidden',
textOverflow: 'ellipsis',
}),
multiValueRemove: () => ({}),
noOptionsMessage: () => ({}),
option: () => ({}),

View File

@ -99,6 +99,8 @@ export interface SelectCommonProps<T> {
) => boolean;
/** Message to display isLoading=true*/
loadingMessage?: string;
/** Disables wrapping of multi value values when closed */
noMultiValueWrap?: boolean;
}
export interface SelectAsyncProps<T> {

View File

@ -227,7 +227,6 @@ export { FieldArray } from './Forms/FieldArray';
// Select
export { default as resetSelectStyles } from './Select/resetSelectStyles';
export * from './Select/Select';
export { DropdownIndicator } from './Select/DropdownIndicator';
export { getSelectStyles } from './Select/getSelectStyles';
export * from './Select/types';