Select: Improve usability slightly (#47796)

* Select: Improve usability slightly

* Latest change

* Fixed test

* Updated test
This commit is contained in:
Torkel Ödegaard 2022-04-28 12:25:51 +02:00 committed by GitHub
parent d451d02628
commit 4fef50272f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 66 additions and 42 deletions

View File

@ -7,6 +7,7 @@ interface DropdownIndicatorProps {
}
export const DropdownIndicator: React.FC<DropdownIndicatorProps> = ({ isOpen }) => {
const icon = isOpen ? 'angle-up' : 'angle-down';
return <Icon name={icon} />;
const icon = isOpen ? 'search' : 'angle-down';
const size = isOpen ? 'sm' : 'md';
return <Icon name={icon} size={size} />;
};

View File

@ -17,44 +17,42 @@ interface InputControlProps {
innerProps: any;
}
const getInputControlStyles = stylesFactory(
(theme: GrafanaTheme2, invalid: boolean, focused: boolean, disabled: boolean, withPrefix: boolean) => {
const styles = getInputStyles({ theme, invalid });
const getInputControlStyles = stylesFactory((theme: GrafanaTheme2, invalid: boolean, withPrefix: boolean) => {
const styles = getInputStyles({ theme, invalid });
return {
input: cx(
inputPadding(theme),
return {
input: cx(
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`
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`
padding-left: 0;
`
),
prefix: cx(
styles.prefix,
css`
position: relative;
padding-left: 0;
`
),
};
}
);
),
prefix: cx(
styles.prefix,
css`
position: relative;
`
),
};
});
export const InputControl = React.forwardRef<HTMLDivElement, React.PropsWithChildren<InputControlProps>>(
function InputControl({ focused, invalid, disabled, children, innerProps, prefix, ...otherProps }, ref) {
const theme = useTheme2();
const styles = getInputControlStyles(theme, invalid, focused, disabled, !!prefix);
const styles = getInputControlStyles(theme, invalid, !!prefix);
return (
<div className={styles.input} {...innerProps} ref={ref}>
{prefix && <div className={cx(styles.prefix)}>{prefix}</div>}

View File

@ -252,7 +252,7 @@ export function SelectBase<T>({
if (allowCustomValue) {
ReactSelectComponent = Creatable as any;
creatableProps.allowCreateWhileLoading = allowCreateWhileLoading;
creatableProps.formatCreateLabel = formatCreateLabel ?? ((input: string) => `Create: ${input}`);
creatableProps.formatCreateLabel = formatCreateLabel ?? defaultFormatCreateLabel;
creatableProps.onCreateOption = onCreateOption;
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>
);
}

View File

@ -1,7 +1,6 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { components, GroupBase, SingleValueProps } from 'react-select';
import tinycolor from 'tinycolor2';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
@ -22,6 +21,7 @@ const getStyles = (theme: GrafanaTheme2) => {
max-width: 100%;
grid-area: 1 / 1 / 2 / 3;
`;
const spinnerWrapper = css`
width: 16px;
height: 16px;
@ -39,10 +39,14 @@ const getStyles = (theme: GrafanaTheme2) => {
`;
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>;
@ -55,7 +59,10 @@ export const SingleValue = <T extends unknown>(props: Props<T>) => {
const loading = useDelayedSwitch(data.loading || false, { delay: 250, duration: 750 });
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 ? (
<FadeWithImage
loading={loading}

View File

@ -34,7 +34,7 @@ describe('useCreatableSelectPersistedBehaviour', () => {
// we type in the input 'Option 2', which should prompt an option creation
await userEvent.type(input, 'Option 2');
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
await userEvent.click(creatableOption);

View File

@ -26,12 +26,12 @@ describe('MetricSelect', () => {
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} />);
await openMetricSelect();
const input = screen.getByRole('combobox');
await userEvent.type(input, 'new');
await waitFor(() => expect(screen.getByText('Create: new')).toBeInTheDocument());
await userEvent.type(input, 'custom value');
await waitFor(() => expect(screen.getByText('custom value')).toBeInTheDocument());
});
it('shows searched options when typing', async () => {

View File

@ -30,6 +30,12 @@ export function MetricSelect({ query, onChange, onGetMetrics }: Props) {
if (!label) {
return false;
}
// custom value is not a string label but a react node
if (!label.toLowerCase) {
return true;
}
const searchWords = searchQuery.split(splitSeparator);
return searchWords.reduce((acc, cur) => acc && label.toLowerCase().includes(cur.toLowerCase()), true);
}, []);