Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(native-filters): Adjust filter components for horizontal mode #22273

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ export interface ChartPropsConfig {
filterState?: FilterState;
/** Set of actual behaviors that this instance of chart should use */
behaviors?: Behavior[];
/** Chart display settings related to current view context */
displaySettings?: JsonObject;
/** Application section of the chart on the screen (in what components/screen it placed) */
appSection?: AppSection;
/** is the chart refreshing its contents */
Expand Down Expand Up @@ -132,6 +134,8 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {

behaviors: Behavior[];

displaySettings?: JsonObject;

appSection?: AppSection;

isRefreshing?: boolean;
Expand All @@ -153,6 +157,7 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
initialValues = {},
queriesData = [],
behaviors = [],
displaySettings = {},
width = DEFAULT_WIDTH,
height = DEFAULT_HEIGHT,
appSection,
Expand All @@ -174,6 +179,7 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
this.ownState = ownState;
this.filterState = filterState;
this.behaviors = behaviors;
this.displaySettings = displaySettings;
this.appSection = appSection;
this.isRefreshing = isRefreshing;
this.inputRef = inputRef;
Expand All @@ -196,6 +202,7 @@ ChartProps.createSelector = function create(): ChartPropsSelector {
input => input.ownState,
input => input.filterState,
input => input.behaviors,
input => input.displaySettings,
input => input.appSection,
input => input.isRefreshing,
input => input.inputRef,
Expand All @@ -213,6 +220,7 @@ ChartProps.createSelector = function create(): ChartPropsSelector {
ownState,
filterState,
behaviors,
displaySettings,
appSection,
isRefreshing,
inputRef,
Expand All @@ -231,6 +239,7 @@ ChartProps.createSelector = function create(): ChartPropsSelector {
filterState,
width,
behaviors,
displaySettings,
appSection,
isRefreshing,
inputRef,
Expand Down
77 changes: 77 additions & 0 deletions superset-frontend/src/components/Select/CustomTag.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* 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 from 'react';
import { Tag as AntdTag } from 'antd';
import { styled } from '@superset-ui/core';
import { useCSSTextTruncation } from 'src/hooks/useTruncation';
import { Tooltip } from '../Tooltip';
import { CustomTagProps } from './types';

const StyledTag = styled(AntdTag)`
& .ant-tag-close-icon {
display: inline-flex;
align-items: center;
margin-left: ${({ theme }) => theme.gridUnit}px;
}

& .tag-content {
overflow: hidden;
text-overflow: ellipsis;
}
`;

// TODO: use antd Tag props instead of any. Currently it's causing a typescript error
const Tag = (props: any) => {
const [tagRef, tagIsTruncated] = useCSSTextTruncation<HTMLSpanElement>();
return (
<Tooltip title={tagIsTruncated ? props.children : null}>
<StyledTag {...props} className="ant-select-selection-item">
<span className="tag-content" ref={tagRef}>
{props.children}
</span>
</StyledTag>
</Tooltip>
);
};

/**
* Custom tag renderer dedicated for oneLine mode
*/
export const oneLineTagRender = (props: CustomTagProps) => {
const { label } = props;

const onPreventMouseDown = (event: React.MouseEvent<HTMLElement>) => {
// if close icon is clicked, stop propagation to avoid opening the dropdown
const target = event.target as HTMLElement;
if (
target.tagName === 'svg' ||
target.tagName === 'path' ||
(target.tagName === 'span' &&
target.className.includes('ant-tag-close-icon'))
) {
event.stopPropagation();
}
};

return (
<Tag onMouseDown={onPreventMouseDown} {...props}>
{label}
</Tag>
);
};
10 changes: 10 additions & 0 deletions superset-frontend/src/components/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ const ARG_TYPES = {
By default label and value.
`,
},
oneLine: {
defaultValue: false,
description: `Sets maxTagCount to 1. The overflow tag is always displayed in
the same line, line wrapping is disabled.
When the dropdown is open, sets maxTagCount to 0,
displays only the overflow tag.
Requires '"mode=multiple"'.
`,
},
};

const mountHeader = (type: String) => {
Expand Down Expand Up @@ -197,6 +206,7 @@ InteractiveSelect.args = {
invertSelection: false,
placeholder: 'Select ...',
optionFilterProps: ['value', 'label', 'custom'],
oneLine: false,
};

InteractiveSelect.argTypes = {
Expand Down
48 changes: 48 additions & 0 deletions superset-frontend/src/components/Select/Select.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,54 @@ test('finds an element with a numeric value and does not duplicate the options',
expect(await querySelectOption('11')).not.toBeInTheDocument();
});

test('Renders only 1 tag and an overflow tag in oneLine mode', () => {
render(
<Select
{...defaultProps}
value={[OPTIONS[0], OPTIONS[1], OPTIONS[2]]}
mode="multiple"
oneLine
/>,
);
expect(screen.getByText(OPTIONS[0].label)).toBeVisible();
expect(screen.queryByText(OPTIONS[1].label)).not.toBeInTheDocument();
expect(screen.queryByText(OPTIONS[2].label)).not.toBeInTheDocument();
expect(screen.getByText('+ 2 ...')).toBeVisible();
});

test('Renders only an overflow tag if dropdown is open in oneLine mode', async () => {
render(
<Select
{...defaultProps}
value={[OPTIONS[0], OPTIONS[1], OPTIONS[2]]}
mode="multiple"
oneLine
/>,
);
await open();

const withinSelector = within(getElementByClassName('.ant-select-selector'));
await waitFor(() => {
expect(
withinSelector.queryByText(OPTIONS[0].label),
).not.toBeInTheDocument();
expect(
withinSelector.queryByText(OPTIONS[1].label),
).not.toBeInTheDocument();
expect(
withinSelector.queryByText(OPTIONS[2].label),
).not.toBeInTheDocument();
expect(withinSelector.getByText('+ 3 ...')).toBeVisible();
});

await type('{esc}');

expect(await withinSelector.findByText(OPTIONS[0].label)).toBeVisible();
expect(withinSelector.queryByText(OPTIONS[1].label)).not.toBeInTheDocument();
expect(withinSelector.queryByText(OPTIONS[2].label)).not.toBeInTheDocument();
expect(withinSelector.getByText('+ 2 ...')).toBeVisible();
});

/*
TODO: Add tests that require scroll interaction. Needs further investigation.
- Fetches more data when scrolling and more data is available
Expand Down
17 changes: 16 additions & 1 deletion superset-frontend/src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
TOKEN_SEPARATORS,
DEFAULT_SORT_COMPARATOR,
} from './constants';
import { oneLineTagRender } from './CustomTag';

/**
* This component is a customized version of the Antdesign 4.X Select component
Expand Down Expand Up @@ -96,6 +97,8 @@ const Select = forwardRef(
tokenSeparators,
value,
getPopupContainer,
oneLine,
maxTagCount: propsMaxTagCount,
...props
}: SelectProps,
ref: RefObject<HTMLInputElement>,
Expand All @@ -106,6 +109,16 @@ const Select = forwardRef(
const [inputValue, setInputValue] = useState('');
const [isLoading, setIsLoading] = useState(loading);
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const [maxTagCount, setMaxTagCount] = useState(
propsMaxTagCount ?? MAX_TAG_COUNT,
);

useEffect(() => {
if (oneLine) {
setMaxTagCount(isDropdownVisible ? 0 : 1);
}
}, [isDropdownVisible, oneLine]);

const mappedMode = isSingleMode
? undefined
: allowNewOptions
Expand Down Expand Up @@ -280,7 +293,7 @@ const Select = forwardRef(
}
headerPosition={headerPosition}
labelInValue={labelInValue}
maxTagCount={MAX_TAG_COUNT}
maxTagCount={maxTagCount}
mode={mappedMode}
notFoundContent={isLoading ? t('Loading...') : notFoundContent}
onDeselect={handleOnDeselect}
Expand Down Expand Up @@ -308,6 +321,8 @@ const Select = forwardRef(
<StyledCheckOutlined iconSize="m" />
)
}
oneLine={oneLine}
tagRender={oneLine ? oneLineTagRender : undefined}
{...props}
ref={ref}
>
Expand Down
25 changes: 22 additions & 3 deletions superset-frontend/src/components/Select/styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ export const StyledContainer = styled.div<{ headerPosition: string }>`
`;

export const StyledSelect = styled(AntdSelect, {
shouldForwardProp: prop => prop !== 'headerPosition',
})<{ headerPosition: string }>`
${({ theme, headerPosition }) => `
shouldForwardProp: prop => prop !== 'headerPosition' && prop !== 'oneLine',
})<{ headerPosition: string; oneLine?: boolean }>`
${({ theme, headerPosition, oneLine }) => `
flex: ${headerPosition === 'left' ? 1 : 0};
&& .ant-select-selector {
border-radius: ${theme.gridUnit}px;
Expand All @@ -52,6 +52,25 @@ export const StyledSelect = styled(AntdSelect, {
.ant-select-arrow .anticon:not(.ant-select-suffix) {
pointer-events: none;
}

${
oneLine &&
`
.ant-select-selection-overflow {
flex-wrap: nowrap;
}

.ant-select-selection-overflow-item:not(.ant-select-selection-overflow-item-rest):not(.ant-select-selection-overflow-item-suffix) {
flex-shrink: 1;
min-width: ${theme.gridUnit * 13}px;
}

.ant-select-selection-overflow-item-suffix {
flex: unset;
min-width: 0px;
}
`
}
`}
`;

Expand Down
13 changes: 13 additions & 0 deletions superset-frontend/src/components/Select/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
SelectValue as AntdSelectValue,
LabeledValue as AntdLabeledValue,
} from 'antd/lib/select';
import { TagProps } from 'antd/lib/tag';

export type RawValue = string | number;

Expand Down Expand Up @@ -140,6 +141,13 @@ export interface BaseSelectProps extends AntdExposedProps {
b: AntdLabeledValue,
search?: string,
) => number;
/**
* Sets maxTagCount to 1. The overflow tag is always displayed in
* the same line, line wrapping is disabled.
* When the dropdown is open, sets maxTagCount to 0,
* displays only the overflow tag.
*/
oneLine?: boolean;

suffixIcon?: ReactNode;

Expand Down Expand Up @@ -203,3 +211,8 @@ export interface AsyncSelectProps extends BaseSelectProps {
*/
onError?: (error: string) => void;
}

export type CustomTagProps = HTMLSpanElement &
TagProps & {
label: ReactNode;
};
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ const HorizontalFormItem = styled(StyledFormItem)`
}

.ant-form-item-control {
width: ${({ theme }) => theme.gridUnit * 40}px;
width: ${({ theme }) => theme.gridUnit * 41}px;
}
`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,12 @@ const FilterValue: React.FC<FilterControlProps> = ({
[filter.dataMask?.filterState, isMissingRequiredValue],
);

const formDataWithDisplayParams = useMemo(
() => ({ ...formData, orientation, overflow }),
[formData, orientation, overflow],
const displaySettings = useMemo(
() => ({
filterBarOrientation: orientation,
isOverflowingFilterBar: overflow,
}),
[orientation, overflow],
);

if (error) {
Expand All @@ -277,7 +280,8 @@ const FilterValue: React.FC<FilterControlProps> = ({
height={HEIGHT}
width="100%"
showOverflow={showOverflow}
formData={formDataWithDisplayParams}
formData={formData}
displaySettings={displaySettings}
parentRef={parentRef}
inputRef={inputRef}
// For charts that don't have datasource we need workaround for empty placeholder
Expand Down
Loading