GrafanaUI: Fix styles for invalid selects & DataSourcePicker (#53476)

* GrafanaUI: fix styles for invalid select & DataSourcePicker

* Apply suggestions from code review

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>

* fix focus issues & tests

* remove unused import

* TypeScript work in progress

* Move react select props to types.ts

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
Co-authored-by: eledobleefe <laura.fernandez@grafana.com>
Co-authored-by: joshhunt <josh@trtr.co>
This commit is contained in:
Giordano Ricci 2022-08-26 13:48:51 +01:00 committed by GitHub
parent a58edc9f5e
commit 26524e3ff1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 77 additions and 108 deletions

View File

@ -1612,11 +1612,7 @@ exports[`better eslint`] = {
[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"], [0, 0, 0, "Unexpected any. Specify a different type.", "12"]
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "16"]
], ],
"packages/grafana-ui/src/components/Select/SelectMenu.tsx:5381": [ "packages/grafana-ui/src/components/Select/SelectMenu.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@ -46,6 +46,7 @@ export interface DataSourcePickerProps {
inputId?: string; inputId?: string;
filter?: (dataSource: DataSourceInstanceSettings) => boolean; filter?: (dataSource: DataSourceInstanceSettings) => boolean;
onClear?: () => void; onClear?: () => void;
invalid?: boolean;
} }
/** /**
@ -186,7 +187,7 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
placeholder={placeholder} placeholder={placeholder}
noOptionsMessage="No datasources found" noOptionsMessage="No datasources found"
value={value ?? null} value={value ?? null}
invalid={!!error} invalid={Boolean(error) || Boolean(this.props.invalid)}
getOptionLabel={(o) => { getOptionLabel={(o) => {
if (o.meta && isUnsignedPluginSignature(o.meta.signature) && o !== value) { if (o.meta && isUnsignedPluginSignature(o.meta.signature) && o !== value) {
return ( return (

View File

@ -1,59 +0,0 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { components, ContainerProps, GroupBase } from 'react-select';
import { GrafanaTheme2 } from '@grafana/data';
import { stylesFactory } from '../../themes';
import { useTheme2 } from '../../themes/ThemeContext';
import { focusCss } from '../../themes/mixins';
import { sharedInputStyle } from '../Forms/commonStyles';
import { getInputStyles } from '../Input/Input';
export const SelectContainer = <Option, isMulti extends boolean, Group extends GroupBase<Option>>(
props: ContainerProps<Option, isMulti, Group> & { isFocused: boolean }
) => {
const { isDisabled, isFocused, children } = props;
const theme = useTheme2();
const styles = getSelectContainerStyles(theme, isFocused, isDisabled);
return (
<components.SelectContainer {...props} className={cx(styles.wrapper, props.className)}>
{children}
</components.SelectContainer>
);
};
const getSelectContainerStyles = stylesFactory((theme: GrafanaTheme2, focused: boolean, disabled: boolean) => {
const styles = getInputStyles({ theme, invalid: false });
return {
wrapper: cx(
styles.wrapper,
sharedInputStyle(theme, false),
focused &&
css`
${focusCss(theme.v1)}
`,
disabled && styles.inputDisabled,
css`
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
min-height: 32px;
height: auto;
max-width: 100%;
/* Input padding is applied to the InputControl so the menu is aligned correctly */
padding: 0;
cursor: ${disabled ? 'not-allowed' : 'pointer'};
`
),
};
});

View File

@ -18,7 +18,6 @@ export function MultiSelect<T>(props: MultiSelectCommonProps<T>) {
export interface AsyncSelectProps<T> extends Omit<SelectCommonProps<T>, 'options'>, SelectAsyncProps<T> { export interface AsyncSelectProps<T> extends Omit<SelectCommonProps<T>, 'options'>, SelectAsyncProps<T> {
// AsyncSelect has options stored internally. We cannot enable plain values as we don't have access to the fetched options // AsyncSelect has options stored internally. We cannot enable plain values as we don't have access to the fetched options
value?: SelectableValue<T> | null; value?: SelectableValue<T> | null;
invalid?: boolean;
} }
export function AsyncSelect<T>(props: AsyncSelectProps<T>) { export function AsyncSelect<T>(props: AsyncSelectProps<T>) {

View File

@ -317,28 +317,28 @@ export function SelectBase<T>({
/> />
); );
}, },
LoadingIndicator(props: any) { LoadingIndicator() {
return <Spinner inline={true} />; return <Spinner inline />;
}, },
LoadingMessage(props: any) { LoadingMessage() {
return <div className={styles.loadingMessage}>{loadingMessage}</div>; return <div className={styles.loadingMessage}>{loadingMessage}</div>;
}, },
NoOptionsMessage(props: any) { NoOptionsMessage() {
return ( return (
<div className={styles.loadingMessage} aria-label="No options provided"> <div className={styles.loadingMessage} aria-label="No options provided">
{noOptionsMessage} {noOptionsMessage}
</div> </div>
); );
}, },
DropdownIndicator(props: any) { DropdownIndicator(props) {
return <DropdownIndicator isOpen={props.selectProps.menuIsOpen} />; return <DropdownIndicator isOpen={props.selectProps.menuIsOpen} />;
}, },
SingleValue(props: any) { SingleValue(props: any) {
return <SingleValue {...props} disabled={disabled} />; return <SingleValue {...props} disabled={disabled} />;
}, },
SelectContainer,
MultiValueContainer: MultiValueContainer, MultiValueContainer: MultiValueContainer,
MultiValueRemove: MultiValueRemove, MultiValueRemove: MultiValueRemove,
SelectContainer,
...components, ...components,
}} }}
styles={selectStyles} styles={selectStyles}

View File

@ -10,19 +10,24 @@ import { focusCss } from '../../themes/mixins';
import { sharedInputStyle } from '../Forms/commonStyles'; import { sharedInputStyle } from '../Forms/commonStyles';
import { getInputStyles } from '../Input/Input'; import { getInputStyles } from '../Input/Input';
// isFocus prop is actually available, but its not in the types for the version we have. import { CustomComponentProps } from './types';
export interface SelectContainerProps<Option, isMulti extends boolean, Group extends GroupBase<Option>>
extends BaseContainerProps<Option, isMulti, Group> { // prettier-ignore
isFocused: boolean; export type SelectContainerProps<Option, isMulti extends boolean, Group extends GroupBase<Option>> =
} BaseContainerProps<Option, isMulti, Group> & CustomComponentProps<Option, isMulti, Group>;
export const SelectContainer = <Option, isMulti extends boolean, Group extends GroupBase<Option>>( export const SelectContainer = <Option, isMulti extends boolean, Group extends GroupBase<Option>>(
props: SelectContainerProps<Option, isMulti, Group> props: SelectContainerProps<Option, isMulti, Group>
) => { ) => {
const { isDisabled, isFocused, children } = props; const {
isDisabled,
isFocused,
children,
selectProps: { invalid = false },
} = props;
const theme = useTheme2(); const theme = useTheme2();
const styles = getSelectContainerStyles(theme, isFocused, isDisabled); const styles = getSelectContainerStyles(theme, isFocused, isDisabled, invalid);
return ( return (
<components.SelectContainer {...props} className={cx(styles.wrapper, props.className)}> <components.SelectContainer {...props} className={cx(styles.wrapper, props.className)}>
@ -31,35 +36,37 @@ export const SelectContainer = <Option, isMulti extends boolean, Group extends G
); );
}; };
const getSelectContainerStyles = stylesFactory((theme: GrafanaTheme2, focused: boolean, disabled: boolean) => { const getSelectContainerStyles = stylesFactory(
const styles = getInputStyles({ theme, invalid: false }); (theme: GrafanaTheme2, focused: boolean, disabled: boolean, invalid: boolean) => {
const styles = getInputStyles({ theme, invalid });
return { return {
wrapper: cx( wrapper: cx(
styles.wrapper, styles.wrapper,
sharedInputStyle(theme, false), sharedInputStyle(theme, invalid),
focused && focused &&
css`
${focusCss(theme.v1)}
`,
disabled && styles.inputDisabled,
css` css`
${focusCss(theme.v1)} position: relative;
`, box-sizing: border-box;
disabled && styles.inputDisabled, /* The display property is set by the styles prop in SelectBase because it's dependant on the width prop */
css` flex-direction: row;
position: relative; flex-wrap: wrap;
box-sizing: border-box; align-items: stretch;
/* The display property is set by the styles prop in SelectBase because it's dependant on the width prop */ justify-content: space-between;
flex-direction: row;
flex-wrap: wrap;
align-items: stretch;
justify-content: space-between;
min-height: 32px; min-height: 32px;
height: auto; height: auto;
max-width: 100%; max-width: 100%;
/* Input padding is applied to the InputControl so the menu is aligned correctly */ /* Input padding is applied to the InputControl so the menu is aligned correctly */
padding: 0; padding: 0;
cursor: ${disabled ? 'not-allowed' : 'pointer'}; cursor: ${disabled ? 'not-allowed' : 'pointer'};
` `
), ),
}; };
}); }
);

View File

@ -1,5 +1,10 @@
import React from 'react'; import React from 'react';
import { ActionMeta as SelectActionMeta, GroupBase, OptionsOrGroups } from 'react-select'; import {
ActionMeta as SelectActionMeta,
CommonProps as ReactSelectCommonProps,
GroupBase,
OptionsOrGroups,
} from 'react-select';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
@ -103,10 +108,13 @@ export interface MultiSelectCommonProps<T> extends Omit<SelectCommonProps<T>, 'o
onChange: (item: Array<SelectableValue<T>>) => {} | void; onChange: (item: Array<SelectableValue<T>>) => {} | void;
} }
// This is the type of *our* SelectBase component, not ReactSelect's prop, although
// they should be mostly compatible.
export interface SelectBaseProps<T> extends SelectCommonProps<T>, SelectAsyncProps<T> { export interface SelectBaseProps<T> extends SelectCommonProps<T>, SelectAsyncProps<T> {
invalid?: boolean; invalid?: boolean;
} }
// This is used for the `renderControl` prop on *our* SelectBase component
export interface CustomControlProps<T> { export interface CustomControlProps<T> {
ref: React.Ref<any>; ref: React.Ref<any>;
isOpen: boolean; isOpen: boolean;
@ -133,3 +141,20 @@ export type SelectOptions<T = any> =
| Array<SelectableValue<T> | SelectableOptGroup<T> | Array<SelectableOptGroup<T>>>; | Array<SelectableValue<T> | SelectableOptGroup<T> | Array<SelectableOptGroup<T>>>;
export type FormatOptionLabelMeta<T> = { context: string; inputValue: string; selectValue: Array<SelectableValue<T>> }; export type FormatOptionLabelMeta<T> = { context: string; inputValue: string; selectValue: Array<SelectableValue<T>> };
// This is the type of `selectProps` our custom components (like SelectContainer, etc) recieve
// It's slightly different to the base react select props because we pass in additional props directly to
// react select
export type ReactSelectProps<Option, IsMulti extends boolean, Group extends GroupBase<Option>> = ReactSelectCommonProps<
Option,
IsMulti,
Group
>['selectProps'] & {
invalid: boolean;
};
// Use this type when implementing custom components for react select.
// See SelectContainerProps in SelectContainer.tsx
export interface CustomComponentProps<Option, isMulti extends boolean, Group extends GroupBase<Option>> {
selectProps: ReactSelectProps<Option, isMulti, Group>;
}