mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -860,8 +860,7 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Do not use any type assertions.", "8"],
|
[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.", "9"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
|
[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.", "11"]
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "12"]
|
|
||||||
],
|
],
|
||||||
"packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx:5381": [
|
"packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { DropdownIndicatorProps } from 'react-select';
|
||||||
|
|
||||||
import { Icon } from '../Icon/Icon';
|
import { Icon } from '../Icon/Icon';
|
||||||
|
|
||||||
interface DropdownIndicatorProps {
|
export function DropdownIndicator({ selectProps }: DropdownIndicatorProps) {
|
||||||
isOpen: boolean;
|
const isOpen = selectProps.menuIsOpen;
|
||||||
}
|
|
||||||
|
|
||||||
export const DropdownIndicator = ({ isOpen }: DropdownIndicatorProps) => {
|
|
||||||
const icon = isOpen ? 'search' : 'angle-down';
|
const icon = isOpen ? 'search' : 'angle-down';
|
||||||
const size = isOpen ? 'sm' : 'md';
|
const size = isOpen ? 'sm' : 'md';
|
||||||
return <Icon name={icon} size={size} />;
|
return <Icon name={icon} size={size} />;
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ const meta: Meta = {
|
|||||||
'renderControl',
|
'renderControl',
|
||||||
'options',
|
'options',
|
||||||
'isOptionDisabled',
|
'isOptionDisabled',
|
||||||
'maxVisibleValues',
|
|
||||||
'aria-label',
|
'aria-label',
|
||||||
'noOptionsMessage',
|
'noOptionsMessage',
|
||||||
'menuPosition',
|
'menuPosition',
|
||||||
@@ -225,7 +224,7 @@ export const MultiSelectBasic: Story = (args) => {
|
|||||||
const [value, setValue] = useState<Array<SelectableValue<string>>>([]);
|
const [value, setValue] = useState<Array<SelectableValue<string>>>([]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div style={{ maxWidth: '450px' }}>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
options={generateOptions()}
|
options={generateOptions()}
|
||||||
value={value}
|
value={value}
|
||||||
@@ -236,13 +235,15 @@ export const MultiSelectBasic: Story = (args) => {
|
|||||||
prefix={getPrefix(args.icon)}
|
prefix={getPrefix(args.icon)}
|
||||||
{...args}
|
{...args}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
MultiSelectBasic.args = {
|
MultiSelectBasic.args = {
|
||||||
isClearable: false,
|
isClearable: false,
|
||||||
closeMenuOnSelect: false,
|
closeMenuOnSelect: false,
|
||||||
maxVisibleValues: 5,
|
maxVisibleValues: 5,
|
||||||
|
noMultiValueWrap: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MultiSelectAsync: Story = (args) => {
|
export const MultiSelectAsync: Story = (args) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
import React, { ComponentProps, useCallback, useEffect, useRef, useState } from 'react';
|
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 ReactAsyncSelect } from 'react-select/async';
|
||||||
import { default as AsyncCreatable } from 'react-select/async-creatable';
|
import { default as AsyncCreatable } from 'react-select/async-creatable';
|
||||||
import Creatable from 'react-select/creatable';
|
import Creatable from 'react-select/creatable';
|
||||||
@@ -25,31 +25,6 @@ import { useCustomSelectStyles } from './resetSelectStyles';
|
|||||||
import { ActionMeta, InputActionMeta, SelectBaseProps } from './types';
|
import { ActionMeta, InputActionMeta, SelectBaseProps } from './types';
|
||||||
import { cleanValue, findSelectedValue, omitDescriptions } from './utils';
|
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 CustomControl = (props: any) => {
|
||||||
const {
|
const {
|
||||||
children,
|
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 = {}>({
|
export function SelectBase<T, Rest = {}>({
|
||||||
allowCustomValue = false,
|
allowCustomValue = false,
|
||||||
allowCreateWhileLoading = false,
|
allowCreateWhileLoading = false,
|
||||||
@@ -145,6 +126,7 @@ export function SelectBase<T, Rest = {}>({
|
|||||||
tabSelectsValue = true,
|
tabSelectsValue = true,
|
||||||
value,
|
value,
|
||||||
virtualized = false,
|
virtualized = false,
|
||||||
|
noMultiValueWrap,
|
||||||
width,
|
width,
|
||||||
isValidNewOption,
|
isValidNewOption,
|
||||||
formatOptionLabel,
|
formatOptionLabel,
|
||||||
@@ -274,6 +256,7 @@ export function SelectBase<T, Rest = {}>({
|
|||||||
showAllSelectedWhenOpen,
|
showAllSelectedWhenOpen,
|
||||||
tabSelectsValue,
|
tabSelectsValue,
|
||||||
value: isMulti ? selectedValue : selectedValue?.[0],
|
value: isMulti ? selectedValue : selectedValue?.[0],
|
||||||
|
noMultiValueWrap,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (allowCustomValue) {
|
if (allowCustomValue) {
|
||||||
@@ -305,31 +288,8 @@ export function SelectBase<T, Rest = {}>({
|
|||||||
MenuList: SelectMenuComponent,
|
MenuList: SelectMenuComponent,
|
||||||
Group: SelectOptionGroup,
|
Group: SelectOptionGroup,
|
||||||
ValueContainer,
|
ValueContainer,
|
||||||
IndicatorsContainer(props: any) {
|
IndicatorsContainer: CustomIndicatorsContainer,
|
||||||
const { selectProps } = props;
|
IndicatorSeparator: IndicatorSeparator,
|
||||||
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 <></>;
|
|
||||||
},
|
|
||||||
Control: CustomControl,
|
Control: CustomControl,
|
||||||
Option: SelectMenuOptions,
|
Option: SelectMenuOptions,
|
||||||
ClearIndicator(props: any) {
|
ClearIndicator(props: any) {
|
||||||
@@ -361,9 +321,7 @@ export function SelectBase<T, Rest = {}>({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
DropdownIndicator(props) {
|
DropdownIndicator: DropdownIndicator,
|
||||||
return <DropdownIndicator isOpen={props.selectProps.menuIsOpen} />;
|
|
||||||
},
|
|
||||||
SingleValue(props: any) {
|
SingleValue(props: any) {
|
||||||
return <SingleValue {...props} isDisabled={disabled} />;
|
return <SingleValue {...props} isDisabled={disabled} />;
|
||||||
},
|
},
|
||||||
@@ -394,3 +352,37 @@ function defaultFormatCreateLabel(input: string) {
|
|||||||
</div>
|
</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 <></>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,8 +30,15 @@ class UnthemedValueContainer extends Component<any & { theme: GrafanaTheme2 }> {
|
|||||||
|
|
||||||
renderContainer(children?: ReactNode) {
|
renderContainer(children?: ReactNode) {
|
||||||
const { isMulti, theme } = this.props;
|
const { isMulti, theme } = this.props;
|
||||||
|
const noWrap = this.props.selectProps?.noMultiValueWrap && !this.props.selectProps?.menuIsOpen;
|
||||||
const styles = getSelectStyles(theme);
|
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>;
|
return <div className={className}>{children}</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ export const getSelectStyles = stylesFactory((theme: GrafanaTheme2) => {
|
|||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
}),
|
}),
|
||||||
|
valueContainerMultiNoWrap: css({
|
||||||
|
flexWrap: 'nowrap',
|
||||||
|
}),
|
||||||
loadingMessage: css({
|
loadingMessage: css({
|
||||||
label: 'grafana-select-loading-message',
|
label: 'grafana-select-loading-message',
|
||||||
padding: theme.spacing(1),
|
padding: theme.spacing(1),
|
||||||
@@ -113,6 +116,8 @@ export const getSelectStyles = stylesFactory((theme: GrafanaTheme2) => {
|
|||||||
padding: theme.spacing(0.25, 0, 0.25, 1),
|
padding: theme.spacing(0.25, 0, 0.25, 1),
|
||||||
color: theme.colors.text.primary,
|
color: theme.colors.text.primary,
|
||||||
fontSize: theme.typography.size.sm,
|
fontSize: theme.typography.size.sm,
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
background: theme.colors.emphasize(theme.colors.background.secondary),
|
background: theme.colors.emphasize(theme.colors.background.secondary),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const generateOptions = (desc = false) => {
|
|||||||
'Ok Vicente',
|
'Ok Vicente',
|
||||||
'Garry Spitz',
|
'Garry Spitz',
|
||||||
'Han Harnish',
|
'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) => ({
|
return values.map<SelectableValue<string>>((name) => ({
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ export default function resetSelectStyles(theme: GrafanaTheme2) {
|
|||||||
maxHeight,
|
maxHeight,
|
||||||
}),
|
}),
|
||||||
multiValue: () => ({}),
|
multiValue: () => ({}),
|
||||||
multiValueLabel: () => ({}),
|
multiValueLabel: () => ({
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}),
|
||||||
multiValueRemove: () => ({}),
|
multiValueRemove: () => ({}),
|
||||||
noOptionsMessage: () => ({}),
|
noOptionsMessage: () => ({}),
|
||||||
option: () => ({}),
|
option: () => ({}),
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ export interface SelectCommonProps<T> {
|
|||||||
) => boolean;
|
) => boolean;
|
||||||
/** Message to display isLoading=true*/
|
/** Message to display isLoading=true*/
|
||||||
loadingMessage?: string;
|
loadingMessage?: string;
|
||||||
|
/** Disables wrapping of multi value values when closed */
|
||||||
|
noMultiValueWrap?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectAsyncProps<T> {
|
export interface SelectAsyncProps<T> {
|
||||||
|
|||||||
@@ -227,7 +227,6 @@ export { FieldArray } from './Forms/FieldArray';
|
|||||||
// Select
|
// Select
|
||||||
export { default as resetSelectStyles } from './Select/resetSelectStyles';
|
export { default as resetSelectStyles } from './Select/resetSelectStyles';
|
||||||
export * from './Select/Select';
|
export * from './Select/Select';
|
||||||
export { DropdownIndicator } from './Select/DropdownIndicator';
|
|
||||||
export { getSelectStyles } from './Select/getSelectStyles';
|
export { getSelectStyles } from './Select/getSelectStyles';
|
||||||
export * from './Select/types';
|
export * from './Select/types';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user