diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterLabel.tsx b/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterLabel.tsx index c68b436a0e497..ce4c32191d0d2 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterLabel.tsx +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/DateFilterLabel.tsx @@ -16,11 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useState, useEffect, useMemo } from 'react'; -import { css, styled, t, useTheme, NO_TIME_RANGE } from '@superset-ui/core'; +import React, { ReactNode, useState, useEffect, useMemo } from 'react'; +import { + css, + styled, + t, + useTheme, + NO_TIME_RANGE, + SupersetTheme, +} from '@superset-ui/core'; import Button from 'src/components/Button'; import ControlHeader from 'src/explore/components/ControlHeader'; -import Label from 'src/components/Label'; import Modal from 'src/components/Modal'; import { Divider } from 'src/components'; import Icons from 'src/components/Icons'; @@ -29,6 +35,7 @@ import { Tooltip } from 'src/components/Tooltip'; import { useDebouncedEffect } from 'src/explore/exploreUtils'; import { SLOW_DEBOUNCE } from 'src/constants'; import { noOp } from 'src/utils/common'; +import { useCSSTextTruncation } from 'src/hooks/useTruncation'; import ControlPopover from '../ControlPopover/ControlPopover'; import { DateFilterControlProps, FrameType } from './types'; @@ -44,6 +51,7 @@ import { CalendarFrame, CustomFrame, AdvancedFrame, + DateLabel, } from './components'; const StyledRangeType = styled(Select)` @@ -120,6 +128,28 @@ const IconWrapper = styled.span` } `; +const getTooltipTitle = ( + isLabelTruncated: boolean, + label: string | undefined, + range: string | undefined, +) => + isLabelTruncated ? ( + <div> + {label && <strong>{label}</strong>} + {range && ( + <div + css={(theme: SupersetTheme) => css` + margin-top: ${theme.gridUnit}px; + `} + > + {range} + </div> + )} + </div> + ) : ( + range || null + ); + export default function DateFilterLabel(props: DateFilterControlProps) { const { onChange, @@ -139,13 +169,14 @@ export default function DateFilterLabel(props: DateFilterControlProps) { const [timeRangeValue, setTimeRangeValue] = useState(value); const [validTimeRange, setValidTimeRange] = useState<boolean>(false); const [evalResponse, setEvalResponse] = useState<string>(value); - const [tooltipTitle, setTooltipTitle] = useState<string>(value); + const [tooltipTitle, setTooltipTitle] = useState<ReactNode | null>(value); const theme = useTheme(); + const [labelRef, labelIsTruncated] = useCSSTextTruncation<HTMLSpanElement>(); useEffect(() => { if (value === NO_TIME_RANGE) { setActualTimeRange(NO_TIME_RANGE); - setTooltipTitle(NO_TIME_RANGE); + setTooltipTitle(null); setValidTimeRange(true); return; } @@ -153,7 +184,7 @@ export default function DateFilterLabel(props: DateFilterControlProps) { if (error) { setEvalResponse(error || ''); setValidTimeRange(false); - setTooltipTitle(value || ''); + setTooltipTitle(value || null); } else { /* HRT == human readable text @@ -172,16 +203,21 @@ export default function DateFilterLabel(props: DateFilterControlProps) { guessedFrame === 'No filter' ) { setActualTimeRange(value); + setTooltipTitle( + getTooltipTitle(labelIsTruncated, value, actualRange), + ); } else { setActualTimeRange(actualRange || ''); - setTooltipTitle(value || ''); + setTooltipTitle( + getTooltipTitle(labelIsTruncated, actualRange, value), + ); } setValidTimeRange(true); } setLastFetchedTimeRange(value); setEvalResponse(actualRange || value); }); - }, [value]); + }, [guessedFrame, labelIsTruncated, labelRef, value]); useDebouncedEffect( () => { @@ -322,12 +358,13 @@ export default function DateFilterLabel(props: DateFilterControlProps) { overlayStyle={{ width: '600px' }} > <Tooltip placement="top" title={tooltipTitle}> - <Label - className="pointer" + <DateLabel + label={actualTimeRange} + isActive={show} + isPlaceholder={actualTimeRange === NO_TIME_RANGE} data-test={DATE_FILTER_TEST_KEY.popoverOverlay} - > - {actualTimeRange} - </Label> + ref={labelRef} + /> </Tooltip> </ControlPopover> ); @@ -335,13 +372,14 @@ export default function DateFilterLabel(props: DateFilterControlProps) { const modalContent = ( <> <Tooltip placement="top" title={tooltipTitle}> - <Label - className="pointer" + <DateLabel onClick={toggleOverlay} + label={actualTimeRange} + isActive={show} + isPlaceholder={actualTimeRange === NO_TIME_RANGE} data-test={DATE_FILTER_TEST_KEY.modalOverlay} - > - {actualTimeRange} - </Label> + ref={labelRef} + /> </Tooltip> {/* the zIndex value is from trying so that the Modal doesn't overlay the AdhocFilter when GENERIC_CHART_AXES is enabled */} <Modal diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/components/DateLabel.tsx b/superset-frontend/src/explore/components/controls/DateFilterControl/components/DateLabel.tsx new file mode 100644 index 0000000000000..2c31f8030f567 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/components/DateLabel.tsx @@ -0,0 +1,100 @@ +/** + * 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, { forwardRef, ReactNode, RefObject } from 'react'; +import { css, styled, useTheme } from '@superset-ui/core'; +import Icons from 'src/components/Icons'; + +export type DateLabelProps = { + label: ReactNode; + isActive?: boolean; + isPlaceholder?: boolean; + onClick?: (event: React.MouseEvent) => void; +}; + +// This is the color that antd components (such as Select or Input) use on hover +// TODO: use theme.colors.primary.base here and in antd components +const ACTIVE_BORDER_COLOR = '#45BED6'; + +const LabelContainer = styled.div<{ + isActive?: boolean; + isPlaceholder?: boolean; +}>` + ${({ theme, isActive, isPlaceholder }) => css` + width: 100%; + height: ${theme.gridUnit * 8}px; + + display: flex; + align-items: center; + flex-wrap: nowrap; + + padding: 0 ${theme.gridUnit * 3}px; + + background-color: ${theme.colors.grayscale.light5}; + + border: 1px solid + ${isActive ? ACTIVE_BORDER_COLOR : theme.colors.grayscale.light2}; + border-radius: ${theme.borderRadius}px; + + cursor: pointer; + + transition: border-color 0.3s cubic-bezier(0.65, 0.05, 0.36, 1); + :hover, + :focus { + border-color: ${ACTIVE_BORDER_COLOR}; + } + + .date-label-content { + color: ${isPlaceholder + ? theme.colors.grayscale.light1 + : theme.colors.grayscale.dark1}; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; + flex-shrink: 1; + white-space: nowrap; + } + + span[role='img'] { + margin-left: auto; + padding-left: ${theme.gridUnit}px; + + & > span[role='img'] { + line-height: 0; + } + } + `} +`; + +export const DateLabel = forwardRef( + (props: DateLabelProps, ref: RefObject<HTMLSpanElement>) => { + const theme = useTheme(); + return ( + <LabelContainer {...props} tabIndex={0}> + <span className="date-label-content" ref={ref}> + {props.label} + </span> + <Icons.CalendarOutlined + iconSize="s" + iconColor={theme.colors.grayscale.base} + /> + </LabelContainer> + ); + }, +); diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/components/index.ts b/superset-frontend/src/explore/components/controls/DateFilterControl/components/index.ts index 0bec821065627..0d46ee7a97d19 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/components/index.ts +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/components/index.ts @@ -20,3 +20,4 @@ export { CommonFrame } from './CommonFrame'; export { CalendarFrame } from './CalendarFrame'; export { CustomFrame } from './CustomFrame'; export { AdvancedFrame } from './AdvancedFrame'; +export { DateLabel } from './DateLabel'; diff --git a/superset-frontend/src/filters/components/Time/TimeFilterPlugin.tsx b/superset-frontend/src/filters/components/Time/TimeFilterPlugin.tsx index 0d4185b7592e6..0e0678d890dbf 100644 --- a/superset-frontend/src/filters/components/Time/TimeFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/Time/TimeFilterPlugin.tsx @@ -38,28 +38,11 @@ const ControlContainer = styled.div<{ display: flex; height: 100%; max-width: 100%; - padding: 2px; - & > span, - & > span:hover { - border: 2px solid transparent; - display: inline-block; - border: ${({ theme, validateStatus }) => - validateStatus && `2px solid ${theme.colors[validateStatus]?.base}`}; - } - &:focus { - & > span { - border: 2px solid - ${({ theme, validateStatus }) => - validateStatus - ? theme.colors[validateStatus]?.base - : theme.colors.primary.base}; - outline: 0; - box-shadow: 0 0 0 2px - ${({ validateStatus }) => - validateStatus - ? 'rgba(224, 67, 85, 12%)' - : 'rgba(32, 167, 201, 0.2)'}; - } + width: 100%; + & > div, + & > div:hover { + ${({ validateStatus, theme }) => + validateStatus && `border-color: ${theme.colors[validateStatus]?.base}`} } `; @@ -99,7 +82,6 @@ export default function TimeFilterPlugin(props: PluginFilterTimeProps) { return props.formData?.inView ? ( <TimeFilterStyles width={width} height={height}> <ControlContainer - tabIndex={-1} ref={inputRef} validateStatus={filterState.validateStatus} onFocus={setFocusedFilter}