mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Select: Improve usability slightly (#47796)
* Select: Improve usability slightly * Latest change * Fixed test * Updated test
This commit is contained in:
parent
d451d02628
commit
4fef50272f
@ -7,6 +7,7 @@ interface DropdownIndicatorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DropdownIndicator: React.FC<DropdownIndicatorProps> = ({ isOpen }) => {
|
export const DropdownIndicator: React.FC<DropdownIndicatorProps> = ({ isOpen }) => {
|
||||||
const icon = isOpen ? 'angle-up' : 'angle-down';
|
const icon = isOpen ? 'search' : 'angle-down';
|
||||||
return <Icon name={icon} />;
|
const size = isOpen ? 'sm' : 'md';
|
||||||
|
return <Icon name={icon} size={size} />;
|
||||||
};
|
};
|
||||||
|
@ -17,44 +17,42 @@ interface InputControlProps {
|
|||||||
innerProps: any;
|
innerProps: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInputControlStyles = stylesFactory(
|
const getInputControlStyles = stylesFactory((theme: GrafanaTheme2, invalid: boolean, withPrefix: boolean) => {
|
||||||
(theme: GrafanaTheme2, invalid: boolean, focused: boolean, disabled: boolean, withPrefix: boolean) => {
|
const styles = getInputStyles({ theme, invalid });
|
||||||
const styles = getInputStyles({ theme, invalid });
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
input: cx(
|
input: cx(
|
||||||
inputPadding(theme),
|
inputPadding(theme),
|
||||||
|
css`
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-right: 0;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
`,
|
||||||
|
withPrefix &&
|
||||||
css`
|
css`
|
||||||
width: 100%;
|
padding-left: 0;
|
||||||
max-width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding-right: 0;
|
|
||||||
position: relative;
|
|
||||||
box-sizing: border-box;
|
|
||||||
`,
|
|
||||||
withPrefix &&
|
|
||||||
css`
|
|
||||||
padding-left: 0;
|
|
||||||
`
|
|
||||||
),
|
|
||||||
prefix: cx(
|
|
||||||
styles.prefix,
|
|
||||||
css`
|
|
||||||
position: relative;
|
|
||||||
`
|
`
|
||||||
),
|
),
|
||||||
};
|
prefix: cx(
|
||||||
}
|
styles.prefix,
|
||||||
);
|
css`
|
||||||
|
position: relative;
|
||||||
|
`
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
export const InputControl = React.forwardRef<HTMLDivElement, React.PropsWithChildren<InputControlProps>>(
|
export const InputControl = React.forwardRef<HTMLDivElement, React.PropsWithChildren<InputControlProps>>(
|
||||||
function InputControl({ focused, invalid, disabled, children, innerProps, prefix, ...otherProps }, ref) {
|
function InputControl({ focused, invalid, disabled, children, innerProps, prefix, ...otherProps }, ref) {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getInputControlStyles(theme, invalid, focused, disabled, !!prefix);
|
const styles = getInputControlStyles(theme, invalid, !!prefix);
|
||||||
return (
|
return (
|
||||||
<div className={styles.input} {...innerProps} ref={ref}>
|
<div className={styles.input} {...innerProps} ref={ref}>
|
||||||
{prefix && <div className={cx(styles.prefix)}>{prefix}</div>}
|
{prefix && <div className={cx(styles.prefix)}>{prefix}</div>}
|
||||||
|
@ -252,7 +252,7 @@ export function SelectBase<T>({
|
|||||||
if (allowCustomValue) {
|
if (allowCustomValue) {
|
||||||
ReactSelectComponent = Creatable as any;
|
ReactSelectComponent = Creatable as any;
|
||||||
creatableProps.allowCreateWhileLoading = allowCreateWhileLoading;
|
creatableProps.allowCreateWhileLoading = allowCreateWhileLoading;
|
||||||
creatableProps.formatCreateLabel = formatCreateLabel ?? ((input: string) => `Create: ${input}`);
|
creatableProps.formatCreateLabel = formatCreateLabel ?? defaultFormatCreateLabel;
|
||||||
creatableProps.onCreateOption = onCreateOption;
|
creatableProps.onCreateOption = onCreateOption;
|
||||||
creatableProps.isValidNewOption = isValidNewOption;
|
creatableProps.isValidNewOption = isValidNewOption;
|
||||||
}
|
}
|
||||||
@ -351,3 +351,15 @@ export function SelectBase<T>({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function defaultFormatCreateLabel(input: string) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||||
|
<div>{input}</div>
|
||||||
|
<div style={{ flexGrow: 1 }} />
|
||||||
|
<div className="muted small" style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||||
|
Hit enter to add
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { components, GroupBase, SingleValueProps } from 'react-select';
|
import { components, GroupBase, SingleValueProps } from 'react-select';
|
||||||
import tinycolor from 'tinycolor2';
|
|
||||||
|
|
||||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
|
|
||||||
@ -22,6 +21,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
grid-area: 1 / 1 / 2 / 3;
|
grid-area: 1 / 1 / 2 / 3;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const spinnerWrapper = css`
|
const spinnerWrapper = css`
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
@ -39,10 +39,14 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const disabled = css`
|
const disabled = css`
|
||||||
color: ${tinycolor(theme.colors.text.disabled).setAlpha(0.64).toString()};
|
color: ${theme.colors.text.disabled};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return { singleValue, spinnerWrapper, spinnerIcon, disabled };
|
const isOpen = css`
|
||||||
|
color: ${theme.colors.text.disabled};
|
||||||
|
`;
|
||||||
|
|
||||||
|
return { singleValue, spinnerWrapper, spinnerIcon, disabled, isOpen };
|
||||||
};
|
};
|
||||||
|
|
||||||
type StylesType = ReturnType<typeof getStyles>;
|
type StylesType = ReturnType<typeof getStyles>;
|
||||||
@ -55,7 +59,10 @@ export const SingleValue = <T extends unknown>(props: Props<T>) => {
|
|||||||
const loading = useDelayedSwitch(data.loading || false, { delay: 250, duration: 750 });
|
const loading = useDelayedSwitch(data.loading || false, { delay: 250, duration: 750 });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<components.SingleValue {...props} className={cx(styles.singleValue, isDisabled && styles.disabled)}>
|
<components.SingleValue
|
||||||
|
{...props}
|
||||||
|
className={cx(styles.singleValue, isDisabled && styles.disabled, props.selectProps.menuIsOpen && styles.isOpen)}
|
||||||
|
>
|
||||||
{data.imgUrl ? (
|
{data.imgUrl ? (
|
||||||
<FadeWithImage
|
<FadeWithImage
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
@ -34,7 +34,7 @@ describe('useCreatableSelectPersistedBehaviour', () => {
|
|||||||
// we type in the input 'Option 2', which should prompt an option creation
|
// we type in the input 'Option 2', which should prompt an option creation
|
||||||
await userEvent.type(input, 'Option 2');
|
await userEvent.type(input, 'Option 2');
|
||||||
const creatableOption = screen.getByLabelText('Select option');
|
const creatableOption = screen.getByLabelText('Select option');
|
||||||
expect(creatableOption).toHaveTextContent('Create: Option 2');
|
expect(creatableOption).toHaveTextContent('Option 2');
|
||||||
|
|
||||||
// we click on the creatable option to trigger its creation
|
// we click on the creatable option to trigger its creation
|
||||||
await userEvent.click(creatableOption);
|
await userEvent.click(creatableOption);
|
||||||
|
@ -26,12 +26,12 @@ describe('MetricSelect', () => {
|
|||||||
await waitFor(() => expect(screen.getAllByLabelText('Select option')).toHaveLength(3));
|
await waitFor(() => expect(screen.getAllByLabelText('Select option')).toHaveLength(3));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows option to create metric when typing', async () => {
|
it('shows option to set custom value when typing', async () => {
|
||||||
render(<MetricSelect {...props} />);
|
render(<MetricSelect {...props} />);
|
||||||
await openMetricSelect();
|
await openMetricSelect();
|
||||||
const input = screen.getByRole('combobox');
|
const input = screen.getByRole('combobox');
|
||||||
await userEvent.type(input, 'new');
|
await userEvent.type(input, 'custom value');
|
||||||
await waitFor(() => expect(screen.getByText('Create: new')).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByText('custom value')).toBeInTheDocument());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows searched options when typing', async () => {
|
it('shows searched options when typing', async () => {
|
||||||
|
@ -30,6 +30,12 @@ export function MetricSelect({ query, onChange, onGetMetrics }: Props) {
|
|||||||
if (!label) {
|
if (!label) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// custom value is not a string label but a react node
|
||||||
|
if (!label.toLowerCase) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const searchWords = searchQuery.split(splitSeparator);
|
const searchWords = searchQuery.split(splitSeparator);
|
||||||
return searchWords.reduce((acc, cur) => acc && label.toLowerCase().includes(cur.toLowerCase()), true);
|
return searchWords.reduce((acc, cur) => acc && label.toLowerCase().includes(cur.toLowerCase()), true);
|
||||||
}, []);
|
}, []);
|
||||||
|
Loading…
Reference in New Issue
Block a user