blob: ffbb102c58d9c482306693a88e643ba49288e70e [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { CSSProperties, ComponentType, ReactNode } from 'react';
import { css, SerializedStyles, ClassNames } from '@emotion/core';
import { SupersetTheme } from '@superset-ui/core';
import {
Styles,
Theme,
SelectComponentsConfig,
components as defaultComponents,
InputProps as ReactSelectInputProps,
} from 'react-select';
import { Props as SelectProps } from 'react-select/src/Select';
import { colors as reactSelectColors } from 'react-select/src/theme';
import { DeepNonNullable } from 'react-select/src/components';
import { OptionType } from 'antd/lib/select';
import { SupersetStyledSelectProps } from './SupersetStyledSelect';
export const DEFAULT_CLASS_NAME = 'Select';
export const DEFAULT_CLASS_NAME_PREFIX = 'Select';
type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>;
};
const colors = (theme: SupersetTheme) => ({
primary: theme.colors.success.base,
danger: theme.colors.error.base,
warning: theme.colors.warning.base,
indicator: theme.colors.info.base,
almostBlack: theme.colors.grayscale.dark1,
grayDark: theme.colors.grayscale.dark1,
grayLight: theme.colors.grayscale.light2,
gray: theme.colors.grayscale.light1,
grayBg: theme.colors.grayscale.light4,
grayBgDarker: theme.colors.grayscale.light3,
grayBgDarkest: theme.colors.grayscale.light2,
grayHeading: theme.colors.grayscale.light1,
menuHover: theme.colors.grayscale.light3,
lightest: theme.colors.grayscale.light5,
darkest: theme.colors.grayscale.dark2,
grayBorder: theme.colors.grayscale.light2,
grayBorderLight: theme.colors.grayscale.light3,
grayBorderDark: theme.colors.grayscale.light1,
textDefault: theme.colors.grayscale.dark1,
textDarkest: theme.colors.grayscale.dark2,
dangerLight: theme.colors.error.light1,
});
export type ThemeConfig = {
borderRadius: number;
// z-index for menu dropdown
// (the same as `@z-index-above-dashboard-charts + 1` in variables.less)
zIndex: number;
colors: {
// add known colors
[key in keyof typeof reactSelectColors]: string;
} &
{
[key in keyof ReturnType<typeof colors>]: string;
} & {
[key: string]: string; // any other colors
};
spacing: Theme['spacing'] & {
// line height and font size must be pixels for easier computation
// of option item height in WindowedMenuList
lineHeight: number;
fontSize: number;
// other relative size must be string
minWidth: string;
};
};
export type PartialThemeConfig = RecursivePartial<ThemeConfig>;
export const defaultTheme: (
theme: SupersetTheme,
) => PartialThemeConfig = theme => ({
borderRadius: theme.borderRadius,
zIndex: 11,
colors: colors(theme),
spacing: {
baseUnit: 3,
menuGutter: 0,
controlHeight: 34,
lineHeight: 19,
fontSize: 14,
minWidth: '7.5em', // just enough to display 'No options'
},
});
// let styles accept serialized CSS, too
type CSSStyles = CSSProperties | SerializedStyles;
type styleFnWithSerializedStyles = (
base: CSSProperties,
state: any,
) => CSSStyles | CSSStyles[];
export type StylesConfig = {
[key in keyof Styles]: styleFnWithSerializedStyles;
};
export type PartialStylesConfig = Partial<StylesConfig>;
export const DEFAULT_STYLES: PartialStylesConfig = {
container: (
provider,
{
theme: {
spacing: { minWidth },
},
},
) => [
provider,
css`
min-width: ${minWidth};
`,
],
placeholder: provider => [
provider,
css`
white-space: nowrap;
`,
],
indicatorSeparator: () => css`
display: none;
`,
indicatorsContainer: provider => [
provider,
css`
i {
width: 1em;
display: inline-block;
}
`,
],
clearIndicator: provider => [
provider,
css`
padding: 4px 0 4px 6px;
`,
],
control: (
provider,
{ isFocused, menuIsOpen, theme: { borderRadius, colors } },
) => {
const isPseudoFocused = isFocused && !menuIsOpen;
let borderColor = colors.grayBorder;
if (isPseudoFocused || menuIsOpen) {
borderColor = colors.grayBorderDark;
}
return [
provider,
css`
border-color: ${borderColor};
box-shadow: ${isPseudoFocused
? 'inset 0 1px 1px rgba(0,0,0,.075), 0 0 0 3px rgba(0,0,0,.1)'
: 'none'};
border-radius: ${menuIsOpen
? `${borderRadius}px ${borderRadius}px 0 0`
: `${borderRadius}px`};
&:hover {
border-color: ${borderColor};
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
}
flex-wrap: nowrap;
padding-left: 1px;
`,
];
},
menu: (provider, { theme: { zIndex } }) => [
provider,
css`
padding-bottom: 2em;
z-index: ${zIndex}; /* override at least multi-page pagination */
width: auto;
min-width: 100%;
max-width: 80vw;
background: none;
box-shadow: none;
border: 0;
`,
],
menuList: (provider, { theme: { borderRadius, colors } }) => [
provider,
css`
background: ${colors.lightest};
border-radius: 0 0 ${borderRadius}px ${borderRadius}px;
border: 1px solid ${colors.grayBorderDark};
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
margin-top: -1px;
border-top-color: ${colors.grayBorderLight};
min-width: 100%;
width: auto;
border-radius: 0 0 ${borderRadius}px ${borderRadius}px;
padding-top: 0;
padding-bottom: 0;
`,
],
option: (
provider,
{
isDisabled,
isFocused,
isSelected,
theme: {
colors,
spacing: { lineHeight, fontSize },
},
},
) => {
let color = colors.textDefault;
let backgroundColor = colors.lightest;
if (isFocused) {
backgroundColor = colors.grayBgDarker;
} else if (isDisabled) {
color = '#ccc';
}
return [
provider,
css`
cursor: pointer;
line-height: ${lineHeight}px;
font-size: ${fontSize}px;
background-color: ${backgroundColor};
color: ${color};
font-weight: ${isSelected ? 600 : 400};
white-space: nowrap;
&:hover:active {
background-color: ${colors.grayBg};
}
`,
];
},
valueContainer: (
provider,
{
isMulti,
hasValue,
theme: {
spacing: { baseUnit },
},
},
) => [
provider,
css`
padding-left: ${isMulti && hasValue ? 1 : baseUnit * 3}px;
`,
],
multiValueLabel: (
provider,
{
theme: {
spacing: { baseUnit },
},
},
) => ({
...provider,
paddingLeft: baseUnit * 1.2,
paddingRight: baseUnit * 1.2,
}),
input: (provider, { selectProps }) => [
provider,
css`
margin-left: 0;
vertical-align: middle;
${selectProps?.isMulti && selectProps?.value?.length
? 'padding: 0 6px; width: 100%'
: 'padding: 0; flex: 1 1 auto;'};
`,
],
menuPortal: base => ({
...base,
zIndex: 1030, // must be same or higher of antd popover
}),
};
const INPUT_TAG_BASE_STYLES = {
background: 'none',
border: 'none',
outline: 'none',
padding: 0,
};
export type SelectComponentsType = Omit<
SelectComponentsConfig<any>,
'Input'
> & {
Input: ComponentType<InputProps>;
};
// react-select is missing selectProps from their props type
// so overwriting it here to avoid errors
export type InputProps = ReactSelectInputProps & {
placeholder?: ReactNode;
selectProps: SelectProps;
autoComplete?: string;
onPaste?: SupersetStyledSelectProps<OptionType>['onPaste'];
inputStyle?: object;
};
const {
ClearIndicator,
DropdownIndicator,
Option,
Input,
SelectContainer,
} = defaultComponents as Required<DeepNonNullable<SelectComponentsType>>;
export const DEFAULT_COMPONENTS: SelectComponentsType = {
SelectContainer: ({ children, ...props }) => {
const {
selectProps: { assistiveText },
} = props;
return (
<div>
<SelectContainer {...props}>{children}</SelectContainer>
{assistiveText && (
<span
css={(theme: SupersetTheme) => ({
marginLeft: 3,
fontSize: theme.typography.sizes.s,
color: theme.colors.grayscale.light1,
})}
>
{assistiveText}
</span>
)}
</div>
);
},
Option: ({ children, innerProps, data, ...props }) => (
<ClassNames>
{({ css }) => (
<Option
{...props}
data={data}
className={css(data && data.style ? data.style : null)}
innerProps={innerProps}
>
{children}
</Option>
)}
</ClassNames>
),
ClearIndicator: props => (
<ClearIndicator {...props}>
<i className="fa">×</i>
</ClearIndicator>
),
DropdownIndicator: props => (
<DropdownIndicator {...props}>
<i
className={`fa fa-caret-${
props.selectProps.menuIsOpen ? 'up' : 'down'
}`}
/>
</DropdownIndicator>
),
Input: (props: InputProps) => {
const { getStyles } = props;
return (
<Input
{...props}
css={getStyles('input', props)}
autoComplete="chrome-off"
inputStyle={INPUT_TAG_BASE_STYLES}
/>
);
},
};
export const VALUE_LABELED_STYLES: PartialStylesConfig = {
valueContainer: (
provider,
{
getValue,
theme: {
spacing: { baseUnit },
},
isMulti,
},
) => ({
...provider,
paddingLeft: getValue().length > 0 ? 1 : baseUnit * 3,
overflow: isMulti && getValue().length > 0 ? 'visible' : 'hidden',
}),
// render single value as is they are multi-value
singleValue: (provider, props) => {
const { getStyles } = props;
return {
...getStyles('multiValue', props),
'.metric-option': getStyles('multiValueLabel', props),
};
},
};