feat: Responsive UI for Big Number with Time Comparison (#27375)

This commit is contained in:
Kamil Gabryjelski 2024-03-04 12:14:09 +01:00 committed by GitHub
parent 2c00cc534c
commit 5de2530e3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 138 additions and 32 deletions

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { createRef, useMemo } from 'react';
import React, { useMemo } from 'react';
import { css, styled, t, useTheme } from '@superset-ui/core';
import { Tooltip } from '@superset-ui/chart-controls';
import {
@ -24,24 +24,33 @@ import {
PopKPIComparisonValueStyleProps,
PopKPIProps,
} from './types';
import { useOverflowDetection } from './useOverflowDetection';
const NumbersContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
overflow: auto;
`;
const ComparisonValue = styled.div<PopKPIComparisonValueStyleProps>`
${({ theme, subheaderFontSize }) => `
font-weight: ${theme.typography.weights.light};
width: 33%;
display: table-cell;
display: flex;
justify-content: center;
font-size: ${subheaderFontSize || 20}px;
text-align: center;
flex: 1 1 0px;
`}
`;
const SymbolWrapper = styled.div<PopKPIComparisonSymbolStyleProps>`
const SymbolWrapper = styled.span<PopKPIComparisonSymbolStyleProps>`
${({ theme, backgroundColor, textColor }) => `
background-color: ${backgroundColor};
color: ${textColor};
padding: ${theme.gridUnit}px ${theme.gridUnit * 2}px;
border-radius: ${theme.gridUnit * 2}px;
display: inline-block;
margin-right: ${theme.gridUnit}px;
`}
`;
@ -61,25 +70,23 @@ export default function PopKPI(props: PopKPIProps) {
comparatorText,
} = props;
const rootElem = createRef<HTMLDivElement>();
const theme = useTheme();
const flexGap = theme.gridUnit * 5;
const wrapperDivStyles = css`
font-family: ${theme.typography.families.sansSerif};
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
padding: ${theme.gridUnit * 4}px;
border-radius: ${theme.gridUnit * 2}px;
align-items: center;
height: ${height}px;
width: ${width}px;
overflow: auto;
`;
const bigValueContainerStyles = css`
font-size: ${headerFontSize || 60}px;
font-weight: ${theme.typography.weights.normal};
text-align: center;
margin-bottom: ${theme.gridUnit * 4}px;
`;
const getArrowIndicatorColor = () => {
@ -135,29 +142,59 @@ export default function PopKPI(props: PopKPIProps) {
tooltipText: t('Percentage difference between the time periods'),
},
],
[prevNumber, valueDifference, percentDifferenceFormattedString],
[
comparatorText,
prevNumber,
valueDifference,
percentDifferenceFormattedString,
],
);
const { isOverflowing, symbolContainerRef, wrapperRef } =
useOverflowDetection(flexGap);
return (
<div ref={rootElem} css={wrapperDivStyles}>
<div css={bigValueContainerStyles}>
{bigNumber}
{percentDifferenceNumber !== 0 && (
<span css={arrowIndicatorStyle}>
{percentDifferenceNumber > 0 ? '↑' : '↓'}
</span>
)}
</div>
<div
css={css`
width: 100%;
display: table;
`}
<div css={wrapperDivStyles} ref={wrapperRef}>
<NumbersContainer
css={
isOverflowing &&
css`
width: fit-content;
margin: auto;
align-items: flex-start;
`
}
>
<div css={bigValueContainerStyles}>
{bigNumber}
{percentDifferenceNumber !== 0 && (
<span css={arrowIndicatorStyle}>
{percentDifferenceNumber > 0 ? '↑' : '↓'}
</span>
)}
</div>
<div
css={css`
display: table-row;
`}
css={[
css`
display: flex;
justify-content: space-around;
gap: ${flexGap}px;
min-width: 0;
flex-shrink: 1;
`,
isOverflowing
? css`
flex-direction: column;
align-items: flex-start;
width: fit-content;
`
: css`
align-items: center;
width: 100%;
`,
]}
ref={symbolContainerRef}
>
{SYMBOLS_WITH_VALUES.map((symbol_with_value, index) => (
<ComparisonValue
@ -182,7 +219,7 @@ export default function PopKPI(props: PopKPIProps) {
</ComparisonValue>
))}
</div>
</div>
</NumbersContainer>
</div>
);
}

View File

@ -103,12 +103,18 @@ const config: ControlPanelConfig = {
controlSetRows: [
['y_axis_format'],
['currency_format'],
[headerFontSize],
[
{
...headerFontSize,
config: { ...headerFontSize.config, default: 0.2 },
},
],
[
{
...subheaderFontSize,
config: {
...subheaderFontSize.config,
default: 0.125,
label: t('Comparison font size'),
},
},

View File

@ -0,0 +1,63 @@
/**
* 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 { useEffect, useRef, useState } from 'react';
import { debounce } from 'lodash';
export const useOverflowDetection = (flexGap: number) => {
const symbolContainerRef = useRef<HTMLDivElement>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const [isOverflowing, setIsOverflowing] = useState(false);
useEffect(() => {
let obs: ResizeObserver;
const symbolContainerElem = symbolContainerRef.current;
const wrapperElem = wrapperRef.current;
if (symbolContainerElem && wrapperElem) {
const symbolContainerChildrenElems = Array.from(
symbolContainerElem.children,
);
obs = new ResizeObserver(
debounce(() => {
const totalChildrenWidth = symbolContainerChildrenElems.reduce(
(acc, element) =>
// take symbol container's child's scroll width to account for the container growing with display: flex
acc + (element.firstElementChild?.scrollWidth ?? 0),
0,
);
if (
totalChildrenWidth +
flexGap * Math.max(symbolContainerChildrenElems.length - 1, 0) >
wrapperElem.clientWidth
) {
setIsOverflowing(true);
} else {
setIsOverflowing(false);
}
}, 500),
);
obs.observe(document.body);
symbolContainerChildrenElems.forEach(elem => {
obs.observe(elem);
});
}
return () => obs?.disconnect();
}, [flexGap]);
return { isOverflowing, symbolContainerRef, wrapperRef };
};