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 }) => {
|
||||
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} />;
|
||||
};
|
||||
|
@ -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>}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
|
@ -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 () => {
|
||||
|
@ -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);
|
||||
}, []);
|
||||
|
Loading…
Reference in New Issue
Block a user