diff --git a/agency-dashboard/src/DashboardView.styles.tsx b/agency-dashboard/src/DashboardView.styles.tsx index 7c855c6dd..a2396126f 100644 --- a/agency-dashboard/src/DashboardView.styles.tsx +++ b/agency-dashboard/src/DashboardView.styles.tsx @@ -16,10 +16,11 @@ // ============================================================================= import { + COMMON_DESKTOP_WIDTH, palette, + TABLET_WIDTH, typography, } from "@justice-counts/common/components/GlobalStyles"; -import { Dropdown } from "@recidiviz/design-system"; import styled from "styled-components/macro"; export const Container = styled.div` @@ -33,12 +34,15 @@ export const Container = styled.div` `; export const LeftPanel = styled.div` - margin-top: 16px; margin-left: 24px; margin-right: 24px; width: 424px; min-width: 424px; margin-right: 126px; + + @media only screen and (max-width: ${COMMON_DESKTOP_WIDTH - 1}px) { + display: none; + } `; export const RightPanel = styled.div` @@ -46,18 +50,23 @@ export const RightPanel = styled.div` flex-grow: 1; flex-direction: column; align-items: stretch; -`; + width: calc(100% - 574px); + height: 100%; + padding-left: 24px; + padding-right: 24px; -export const RightPanelTopContainer = styled.div` - display: flex; + @media only screen and (max-width: ${TABLET_WIDTH - 1}px) { + padding-left: 16px; + padding-right: 16px; + } `; -export const LeftPanelBackButtonContainer = styled.div` +export const BackButtonContainer = styled.div` ${typography.sizeCSS.normal} - float: left; padding-top: 8px; padding-right: 8px; padding-bottom: 8px; + margin-top: 16px; display: flex; align-items: center; gap: 8px; @@ -76,28 +85,66 @@ export const LeftPanelBackButtonContainer = styled.div` } `; +export const RightPanelBackButtonContainer = styled(BackButtonContainer)` + @media only screen and (min-width: ${COMMON_DESKTOP_WIDTH}px) { + display: none; + } +`; + export const MetricTitle = styled.div` ${typography.sizeCSS.headline} - margin-top: 86px; + margin-top: 36px; + margin-bottom: 16px; + hyphens: auto; + overflow-wrap: break-word; `; -export const MetricOverviewTitle = styled.div` - ${typography.sizeCSS.large} - margin-top: 48px; +export const RightPanelMetricTitle = styled(MetricTitle)` + margin-bottom: 40px; - &::after { - content: "Metric Overview"; + @media only screen and (min-width: ${COMMON_DESKTOP_WIDTH}px) { + display: none; + } + + @media only screen and (max-width: ${TABLET_WIDTH - 1}px) { + ${typography.sizeCSS.title} + margin-bottom: 16px; } `; export const MetricOverviewContent = styled.div` ${typography.sizeCSS.medium} + + @media only screen and (max-width: ${TABLET_WIDTH - 1}px) { + ${typography.sizeCSS.normal} + } +`; + +export const RightPanelMetricOverviewContent = styled(MetricOverviewContent)` margin-top: 16px; + + @media only screen and (min-width: ${COMMON_DESKTOP_WIDTH}px) { + display: none; + } `; export const MetricOverviewActionsContainer = styled.div` display: flex; margin-top: 24px; + padding-bottom: 32px; +`; + +export const RightPanelMetricOverviewActionsContainer = styled( + MetricOverviewActionsContainer +)` + @media only screen and (min-width: ${COMMON_DESKTOP_WIDTH}px) { + display: none; + } + + @media only screen and (max-width: ${TABLET_WIDTH - 1}px) { + flex-direction: column; + padding-bottom: 64px; + } `; export const MetricOverviewActionButtonContainer = styled.div` @@ -117,51 +164,13 @@ export const MetricOverviewActionButtonContainer = styled.div` &:first-child { padding-left: 0px; } -`; - -export const MetricOverviewActionButtonText = styled.div` - ${typography.sizeCSS.normal} - margin-left: 8px; -`; - -export const AllMetricsButtonContainer = styled.div` - padding-left: 16px; - padding-right: 16px; - padding-top: 8px; - padding-bottom: 8px; - display: flex; - align-items: center; - border-radius: 2px; - background: ${palette.solid.blue}; - gap: 8px; - color: ${palette.solid.white}; - &:hover { - cursor: pointer; - background: ${palette.solid.darkblue}; + @media only screen and (max-width: ${TABLET_WIDTH - 1}px) { + padding-left: 0px; } `; -export const AllMetricsButtonText = styled.div` +export const MetricOverviewActionButtonText = styled.div` ${typography.sizeCSS.normal} - font-weight: 400; - - &::after { - content: "Select Metric"; - } -`; - -export const ExtendedDropdown = styled(Dropdown)` - & > button { - margin-top: 8px; - margin-bottom: 8px; - margin-left: 15px; - transition-duration: 0ms; - background: none; - padding: 0; - border: none; - } - &:hover > button { - background: none; - } + margin-left: 8px; `; diff --git a/agency-dashboard/src/DashboardView.tsx b/agency-dashboard/src/DashboardView.tsx index 3bd326844..d6f3cfaf4 100644 --- a/agency-dashboard/src/DashboardView.tsx +++ b/agency-dashboard/src/DashboardView.tsx @@ -16,40 +16,48 @@ // ============================================================================= import { ReactComponent as DownloadIcon } from "@justice-counts/common/assets/download-icon.svg"; -import { ReactComponent as GridIcon } from "@justice-counts/common/assets/grid-icon.svg"; import { ReactComponent as InfoIcon } from "@justice-counts/common/assets/info-icon.svg"; import { ReactComponent as LeftArrowIcon } from "@justice-counts/common/assets/left-arrow-icon.svg"; import { ReactComponent as ShareIcon } from "@justice-counts/common/assets/share-icon.svg"; import { DatapointsView } from "@justice-counts/common/components/DataViz/DatapointsView"; -import { ExtendedDropdownMenuItem } from "@justice-counts/common/components/DataViz/DatapointsView.styles"; -import { DropdownMenu, DropdownToggle } from "@recidiviz/design-system"; +import { COMMON_DESKTOP_WIDTH } from "@justice-counts/common/components/GlobalStyles"; import { observer } from "mobx-react-lite"; -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { useLocation, useNavigate, useParams } from "react-router-dom"; import { - AllMetricsButtonContainer as SelectMetricButtonContainer, - AllMetricsButtonText, + BackButtonContainer, Container, - ExtendedDropdown, LeftPanel, - LeftPanelBackButtonContainer, MetricOverviewActionButtonContainer, MetricOverviewActionButtonText, MetricOverviewActionsContainer, MetricOverviewContent, - MetricOverviewTitle, MetricTitle, RightPanel, - RightPanelTopContainer, + RightPanelBackButtonContainer, + RightPanelMetricOverviewActionsContainer, + RightPanelMetricOverviewContent, + RightPanelMetricTitle, } from "./DashboardView.styles"; import { HeaderBar } from "./Header/HeaderBar"; import { useStore } from "./stores"; -const LeftPanelBackButton = () => ( - +const getScreenWidth = () => + window.innerWidth || + document.documentElement.clientWidth || + document.body.clientWidth; + +const BackButton = ({ onClick }: { onClick: () => void }) => ( + + + +); + +const RightPanelBackButton = ({ onClick }: { onClick: () => void }) => ( + - + ); const MetricOverviewActionInfoButton = () => ( @@ -75,37 +83,9 @@ const MetricOverviewActionShareButton = () => ( ); -const SelectMetricButton = () => ( - - - - -); - -const SelectMetricButtonDropdown: React.FC<{ - onSelect: (metricKey: string) => void; - options: string[]; -}> = ({ onSelect, options }) => ( - - - - - - {options.map((value) => ( - { - onSelect(value); - }} - > - {value} - - ))} - - -); - const DashboardView = () => { + const [shouldResizeChartHeight, setShouldResizeChartHeight] = + useState(getScreenWidth() >= COMMON_DESKTOP_WIDTH); const navigate = useNavigate(); const params = useParams(); const agencyId = Number(params.id); @@ -131,6 +111,28 @@ const DashboardView = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [datapointsStore.loading]); + useEffect(() => { + const resizeListener = () => { + // change width from the state object + if (shouldResizeChartHeight && getScreenWidth() < COMMON_DESKTOP_WIDTH) { + setShouldResizeChartHeight(false); + } else if ( + !shouldResizeChartHeight && + getScreenWidth() >= COMMON_DESKTOP_WIDTH + ) { + setShouldResizeChartHeight(true); + } + }; + // set resize listener + window.addEventListener("resize", resizeListener); + + // clean up function + return () => { + // remove resize listener + window.removeEventListener("resize", resizeListener); + }; + }, [shouldResizeChartHeight]); + if ( !metricKey || (!datapointsStore.loading && @@ -151,30 +153,25 @@ const DashboardView = () => { - + navigate(`/agency/${agencyId}`)} /> {datapointsStore.metricKeyToDisplayName[metricKey] || metricKey} - Measures the number of individuals with at least one parole violation during the reporting period. - - + + - - - navigate(`/agency/${agencyId}/dashboard?metric=${metric}`) - } - options={metricNames} - /> - + navigate(`/agency/${agencyId}`)} /> + + {datapointsStore.metricKeyToDisplayName[metricKey] || metricKey} + { dimensionNamesByDisaggregation={ datapointsStore.dimensionNamesByMetricAndDisaggregation[metricKey] } + metricNames={metricNames} + onMetricsSelect={(metric) => + navigate(`/agency/${agencyId}/dashboard?metric=${metric}`) + } + resizeHeight={shouldResizeChartHeight} /> + + Measures the number of individuals with at least one parole violation + during the reporting period. + + + + + + ); diff --git a/agency-dashboard/src/Header/HeaderBar.styles.tsx b/agency-dashboard/src/Header/HeaderBar.styles.tsx index 8dd19743e..c6cfa66fc 100644 --- a/agency-dashboard/src/Header/HeaderBar.styles.tsx +++ b/agency-dashboard/src/Header/HeaderBar.styles.tsx @@ -18,6 +18,7 @@ import { HEADER_BAR_HEIGHT, palette, + TABLET_WIDTH, typography, } from "@justice-counts/common/components/GlobalStyles"; import styled from "styled-components/macro"; @@ -59,6 +60,10 @@ export const Logo = styled.img` export const HeaderTitle = styled.div` flex-grow: 1; padding-left: 16px; + + @media only screen and (max-width: ${TABLET_WIDTH - 1}px) { + display: none; + } `; export const HeaderButtonsContainer = styled.div` diff --git a/agency-dashboard/src/Header/HeaderBar.tsx b/agency-dashboard/src/Header/HeaderBar.tsx index a7acbd35e..4018f57ba 100644 --- a/agency-dashboard/src/Header/HeaderBar.tsx +++ b/agency-dashboard/src/Header/HeaderBar.tsx @@ -16,7 +16,7 @@ // ============================================================================= import logo from "@justice-counts/common/assets/jc-logo-vector.png"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { AboutModal } from "./AboutModal"; @@ -41,6 +41,16 @@ export const HeaderBar = () => { setAboutModalVisible(false); }; + /** Prevent body from scrolling when modal is open */ + useEffect(() => { + if (aboutModalVisible) { + document.body.style.overflow = "hidden"; + } + return () => { + document.body.style.overflow = "unset"; + }; + }, [aboutModalVisible]); + return ( {aboutModalVisible && } diff --git a/common/assets/share-icon.svg b/common/assets/share-icon.svg index 2f796dada..98569909c 100644 --- a/common/assets/share-icon.svg +++ b/common/assets/share-icon.svg @@ -1,4 +1,12 @@ - - - + + + + + + + + + + + diff --git a/common/components/DataViz/BarChart.tsx b/common/components/DataViz/BarChart.tsx index f4de31689..f5b7b70a8 100644 --- a/common/components/DataViz/BarChart.tsx +++ b/common/components/DataViz/BarChart.tsx @@ -29,17 +29,22 @@ import styled from "styled-components/macro"; import { Datapoint } from "../../types"; import { rem } from "../../utils"; -import { palette } from "../GlobalStyles"; +import { COMMON_DESKTOP_WIDTH, palette } from "../GlobalStyles"; import Tooltip from "./Tooltip"; import { splitUtcString } from "./utils"; const MAX_BAR_SIZE = 150; const ChartContainer = styled.div` - display: flex; - flex-grow: 1; - justify-content: center; - align-items: center; + width: 100%; + + @media only screen and (min-width: ${COMMON_DESKTOP_WIDTH}px) { + height: calc(100% - 214px); + display: flex; + flex-grow: 1; + justify-content: center; + align-items: center; + } `; const NoReportedData = styled.div` @@ -129,7 +134,13 @@ const ResponsiveBarChart: React.FC<{ data: Datapoint[]; dimensionNames: string[]; percentageView?: boolean; -}> = ({ data, dimensionNames, percentageView = false }) => { + resizeHeight?: boolean; +}> = ({ + data, + dimensionNames, + percentageView = false, + resizeHeight = false, +}) => { const isAnnual = data[0]?.frequency === "ANNUAL"; const renderBarDefinitions = () => { // each Recharts Bar component defines a category type in the stacked bar chart @@ -157,9 +168,19 @@ const ResponsiveBarChart: React.FC<{ return barDefinitions; }; + const responsiveContainerProps = resizeHeight + ? { + width: "100%", + height: "100%", + } + : { + width: "100%", + height: 500, + }; + return ( - + = ({ {value} ); + +export const DatapointsViewControlsRow = styled.div` + display: flex; + flex-direction: row; + align-items: center; + gap: 24px; + + @media only screen and (max-width: ${TABLET_WIDTH - 1}px) { + display: none; + } +`; + +export const MobileFiltersRow = styled.div` + display: none; + + @media only screen and (max-width: ${TABLET_WIDTH - 1}px) { + display: flex; + border-top: 1px solid ${palette.highlight.grey9}; + padding-top: 8px; + } +`; + +export const MobileFiltersButton = styled.div` + ${typography.sizeCSS.small} + padding: 4px 10px; + border: 1px solid ${palette.highlight.grey4}; + border-radius: 24px; + + &::after { + content: "▾ Filters"; + } + + &:hover { + cursor: pointer; + opacity: 0.8; + } +`; + +export const SelectMetricsButtonContainer = styled.div` + padding-left: 16px; + padding-right: 16px; + padding-top: 8px; + padding-bottom: 8px; + display: flex; + align-items: center; + border-radius: 2px; + background: ${palette.solid.blue}; + gap: 8px; + color: ${palette.solid.white}; + + &:hover { + cursor: pointer; + background: ${palette.solid.darkblue}; + } +`; + +export const SelectMetricsButtonText = styled.div` + ${typography.sizeCSS.normal} + font-weight: 400; + + &::after { + content: "Select Metric"; + } +`; + +export const MobileSelectMetricsButtonContainer = styled.div` + display: flex; + justify-content: flex-start; + path { + fill: ${palette.solid.darkgrey}; + } + position: fixed; + bottom: 24px; + justify-content: center; + left: 0; + right: 0; + pointer-events: none; + + @media only screen and (min-width: ${TABLET_WIDTH}px) { + display: none; + } +`; + +export const MobileSelectMetricsButton = styled.div` + pointer-events: auto; + display: flex; + align-items: center; + gap: 8px; + padding: 4px 10px; + border: 1px solid ${palette.highlight.grey4}; + border-radius: 24px; + background: ${palette.solid.white}; + + &:hover { + cursor: pointer; + opacity: 0.8; + } +`; + +export const ExtendedDropdown = styled(Dropdown)` + & > button { + margin-top: 8px; + margin-bottom: 8px; + transition-duration: 0ms; + background: none; + padding: 0; + border: none; + } + &:hover > button { + background: none; + } +`; + +export const MobileSelectMetricsModalContainer = styled.div` + z-index: 3; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: ${palette.solid.blue}; + color: ${palette.solid.white}; + overflow-y: auto; +`; diff --git a/common/components/DataViz/DatapointsView.tsx b/common/components/DataViz/DatapointsView.tsx index 63373e9a0..3894740a1 100644 --- a/common/components/DataViz/DatapointsView.tsx +++ b/common/components/DataViz/DatapointsView.tsx @@ -15,6 +15,7 @@ // along with this program. If not, see . // ============================================================================= +import { ReactComponent as GridIcon } from "@justice-counts/common/assets/grid-icon.svg"; import BarChart from "@justice-counts/common/components/DataViz/BarChart"; import Legend from "@justice-counts/common/components/DataViz/Legend"; import { @@ -24,14 +25,25 @@ import { DataVizTimeRangesMap, DimensionNamesByDisaggregation, } from "@justice-counts/common/types"; +import { DropdownMenu, DropdownToggle } from "@recidiviz/design-system"; import React, { useEffect } from "react"; import { DatapointsViewContainer, DatapointsViewControlsContainer, DatapointsViewControlsDropdown, + DatapointsViewControlsRow, + ExtendedDropdown, + ExtendedDropdownMenuItem, MetricInsight, MetricInsightsRow, + MobileFiltersButton, + MobileFiltersRow, + MobileSelectMetricsButton, + MobileSelectMetricsButtonContainer, + MobileSelectMetricsModalContainer, + SelectMetricsButtonContainer, + SelectMetricsButtonText, } from "./DatapointsView.styles"; import { filterByTimeRange, @@ -45,12 +57,50 @@ import { const noDisaggregationOption = "None"; +const SelectMetricButton = () => ( + + + + +); + +const SelectMetricButtonDropdown: React.FC<{ + onSelect?: (metricKey: string) => void; + options?: string[]; +}> = ({ onSelect, options }) => ( + + + + + + {options?.map((value) => ( + { + if (onSelect) { + onSelect(value); + } + }} + > + {value} + + ))} + + +); + export const DatapointsView: React.FC<{ datapointsGroupedByAggregateAndDisaggregations: DatapointsGroupedByAggregateAndDisaggregations; dimensionNamesByDisaggregation: DimensionNamesByDisaggregation; + metricNames?: string[]; + onMetricsSelect?: (metric: string) => void; + resizeHeight?: boolean; }> = ({ datapointsGroupedByAggregateAndDisaggregations, dimensionNamesByDisaggregation, + metricNames, + onMetricsSelect, + resizeHeight = false, }) => { const [selectedTimeRange, setSelectedTimeRange] = React.useState("All"); @@ -58,6 +108,8 @@ export const DatapointsView: React.FC<{ React.useState(noDisaggregationOption); const [datapointsViewSetting, setDatapointsViewSetting] = React.useState("Count"); + const [mobileSelectMetricsVisible, setMobileSelectMetricsVisible] = + React.useState(false); const data = (selectedDisaggregation !== noDisaggregationOption && @@ -95,6 +147,16 @@ export const DatapointsView: React.FC<{ // eslint-disable-next-line react-hooks/exhaustive-deps }, [datapointsGroupedByAggregateAndDisaggregations]); + /** Prevent body from scrolling when modal is open */ + useEffect(() => { + if (mobileSelectMetricsVisible) { + document.body.style.overflow = "hidden"; + } + return () => { + document.body.style.overflow = "unset"; + }; + }, [mobileSelectMetricsVisible]); + const renderChartForMetric = () => { return ( ); }; @@ -183,10 +246,28 @@ export const DatapointsView: React.FC<{ return ( - {renderDataVizControls()} - {renderMetricInsightsRow()} + + + {renderDataVizControls()} + + + + {renderChartForMetric()} + {renderMetricInsightsRow()} {renderLegend()} + + setMobileSelectMetricsVisible(true)} + > + + + + + {mobileSelectMetricsVisible && } ); }; diff --git a/common/components/DataViz/Legend.tsx b/common/components/DataViz/Legend.tsx index 0fa7b4f11..30c088d83 100644 --- a/common/components/DataViz/Legend.tsx +++ b/common/components/DataViz/Legend.tsx @@ -20,16 +20,13 @@ import styled from "styled-components/macro"; import { palette } from "../GlobalStyles"; -const EmptyLegendContainer = styled.div` +const LegendContainer = styled.div` display: flex; flex-direction: row; align-items: center; flex-wrap: wrap; margin-bottom: 20px; height: 50px; -`; - -const LegendContainer = styled(EmptyLegendContainer)` border-top: 1px solid rgba(23, 28, 43, 0.6); padding: 16px 0; border-bottom: 1px solid rgba(23, 28, 43, 0.6); @@ -72,7 +69,7 @@ const Legend: React.FC<{ ); } - return ; + return null; }; export default Legend; diff --git a/common/components/GlobalStyles/constants.ts b/common/components/GlobalStyles/constants.ts index 416d9dd54..b9b66f67a 100644 --- a/common/components/GlobalStyles/constants.ts +++ b/common/components/GlobalStyles/constants.ts @@ -17,3 +17,4 @@ export const HEADER_BAR_HEIGHT = 64; export const TABLET_WIDTH = 834; +export const COMMON_DESKTOP_WIDTH = 1366;