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.", "10"],
[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.", "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"]
[0, 0, 0, "Unexpected any. Specify a different type.", "12"]
],
"packages/grafana-ui/src/components/Select/SelectMenu.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@ -46,6 +46,7 @@ export interface DataSourcePickerProps {
inputId?: string;
filter?: (dataSource: DataSourceInstanceSettings) => boolean;
onClear?: () => void;
invalid?: boolean;
}
/**
@ -186,7 +187,7 @@ export class DataSourcePicker extends PureComponent<DataSourcePickerProps, DataS
placeholder={placeholder}
noOptionsMessage="No datasources found"
value={value ?? null}
invalid={!!error}
invalid={Boolean(error) || Boolean(this.props.invalid)}
getOptionLabel={(o) => {
if (o.meta && isUnsignedPluginSignature(o.meta.signature) && o !== value) {
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> {
// AsyncSelect has options stored internally. We cannot enable plain values as we don't have access to the fetched options
value?: SelectableValue<T> | null;
invalid?: boolean;
}
export function AsyncSelect<T>(props: AsyncSelectProps<T>) {

View File

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

View File

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

View File

@ -1,5 +1,10 @@
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';
@ -103,10 +108,13 @@ export interface MultiSelectCommonProps<T> extends Omit<SelectCommonProps<T>, 'o
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> {
invalid?: boolean;
}
// This is used for the `renderControl` prop on *our* SelectBase component
export interface CustomControlProps<T> {
ref: React.Ref<any>;
isOpen: boolean;
@ -133,3 +141,20 @@ export type SelectOptions<T = any> =
| Array<SelectableValue<T> | SelectableOptGroup<T> | Array<SelectableOptGroup<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>;
}