From ebe5239414b8ce67ce8bc5232ed6307bda6691ad Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" Date: Fri, 2 Feb 2024 14:19:48 +0100 Subject: [PATCH 01/66] WIP new legend --- .../src/widgets/legend/LegendWidgetUI.d.ts | 36 ++ .../src/widgets/new-legend/LegendWidgetUI.js | 430 ++++++++++++++++++ .../react-ui/storybook/.storybook/preview.js | 44 +- .../widgetsUI/NewLegendWidgetUI.stories.js | 97 ++++ .../stories/widgetsUI/legendFixtures.js | 186 ++++++++ tsconfig.json | 2 +- 6 files changed, 784 insertions(+), 11 deletions(-) create mode 100644 packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js create mode 100644 packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js create mode 100644 packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts index ed791401c..683d57cda 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts @@ -1,3 +1,5 @@ +import type React from 'react' + export enum LEGEND_TYPES { CATEGORY = 'category', ICON = 'icon', @@ -6,3 +8,37 @@ export enum LEGEND_TYPES { PROPORTION = 'proportion', CUSTOM = 'custom' } + +export type LegendData = { + id: string; + title?: string; + visible?: boolean; // layer visibility state + switchable?: boolean; // layer visibility state can be toggled on/off + collapsed?: boolean; // layer collapsed state + collapsible?: boolean; // layer collapsed state can be toggled on/off + opacity?: number; // layer opacity percentage + showOpacityControl?: boolean; // layer opacity percentage can be modified + helperText?: React.ReactNode; // note to show below all legend items + minZoom?: number; // min zoom at which layer is displayed + maxZoom?: number; // max zoom at which layer is displayed + legend?: LegendItemData | LegendItemData[]; +}; + +export type LegendItemData = { + type: LEGEND_TYPES; + children?: React.ReactNode; + attr?: React.ReactNode; // subtitle + colors?: string[]; + labels?: (string | number)[]; + icons?: string[]; + select: LegendItemSelectConfig +}; + +export type LegendItemSelectConfig = { + label: string; + value: string; + options: { + label: string; + value: string; + }[]; +}; diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js new file mode 100644 index 000000000..092c0cb14 --- /dev/null +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js @@ -0,0 +1,430 @@ +import PropTypes from 'prop-types'; +import { + Box, + Collapse, + IconButton, + InputAdornment, + Paper, + Popover, + Slider, + TextField, + Typography +} from '@mui/material'; +import { LEGEND_TYPES, LegendCategories, LegendIcon, LegendRamp } from '@carto/react-ui'; +import EyeIcon from '@mui/icons-material/VisibilityOutlined'; +import EyeOffIcon from '@mui/icons-material/VisibilityOffOutlined'; +import CloseIcon from '@mui/icons-material/Close'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import LayerIcon from '@mui/icons-material/LayersOutlined'; +import { useRef, useState } from 'react'; + +/** + * Returns a number whose value is limited to the given range. + * + * @param {Number} val The initial value + * @param {Number} min The lower boundary + * @param {Number} max The upper boundary + * @returns {Number} A number in the range (min, max) + */ +const clamp = (val, min, max) => Math.min(Math.max(val, min), max); + +export const styles = { + legendToggleOpen: { + borderBottom: (theme) => `1px solid ${theme.palette.divider}` + }, + legendToggle: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + pl: 2, + pr: 1, + py: 1 + }, + legendItemList: { + overflow: 'auto', + maxHeight: `calc(100% - 12px)` + }, + legendItem: { + '&:not(:first-of-type)': { + borderTop: (theme) => `1px solid ${theme.palette.divider}` + } + }, + legendItemHeader: { + p: 1.5, + pr: 2, + gap: 0.5, + display: 'flex', + justifyContent: 'space-between', + position: 'relative' + }, + legendItemBody: { + px: 2 + // '& [data-testid="categories-legend"] > .MuiGrid-root': { + // paddingTop: '6px', + // paddingBottom: '6px', + // }, + // '& [data-testid="icon-legend"] > .MuiGrid-root': { + // paddingTop: '2px', + // paddingBottom: '2px', + // '& > .MuiBox-root': { + // width: '20px', + // height: '20px', + // marginRight: '8px', + // }, + // '& img': { + // display: 'block', + // margin: 'auto', + // width: 'auto', + // height: '20px', + // }, + // }, + }, + opacityControl: { + display: 'flex', + gap: 2, + alignItems: 'center', + p: 1, + width: 208 + }, + layerOptions: { + background: (theme) => theme.palette.background.default, + px: 2, + py: 1, + m: 2 + }, + opacityInput: { + display: 'flex', + width: '60px', + flexShrink: 0 + }, + 'top-left': { + top: 0, + left: 0 + }, + 'top-right': { + top: 0, + right: 0 + }, + 'bottom-left': { + bottom: 0, + left: 0 + }, + 'bottom-right': { + bottom: 0, + right: 0 + } +}; + +const EMPTY_OBJ = {}; +const EMPTY_FN = () => {}; +const EMPTY_ARR = []; + +/** + * @param {object} props + * @param {Object.} [props.customLegendTypes] - Allow to customise by default legend types that can be rendered. + * @param {import('../legend/LegendWidgetUI').LegendData[]} [props.layers] - Array of layer objects from redux store. + * @param {boolean} [props.collapsed] - Collapsed state for whole legend widget. + * @param {(collapsed: boolean) => void} props.onChangeCollapsed - Callback function for collapsed state change. + * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} props.onChangeLegendRowCollapsed - Callback function for layer visibility change. + * @param {({ id, opacity }: { id: string, opacity: number }) => void} props.onChangeOpacity - Callback function for layer opacity change. + * @param {({ id, visible }: { id: string, visible: boolean }) => void} props.onChangeVisibility - Callback function for layer collapsed state change. + * @param {string[]} [props.layerOrder] - Array of layer identifiers. Defines the order of layer legends. [] by default. + * @param {string} [props.title] - Title of the toggle button when widget is open. + * @param {'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'} [props.position] - Position of the widget. + * @param {number} [props.maxZoom] - Global maximum zoom level for the map. + * @param {number} [props.minZoom] - Global minimum zoom level for the map. + * @returns {React.ReactNode} + */ +function NewLegendWidgetUI({ + customLegendTypes = EMPTY_OBJ, + layers = EMPTY_ARR, + collapsed = true, + onChangeCollapsed = EMPTY_FN, + onChangeVisibility = EMPTY_FN, + onChangeOpacity = EMPTY_FN, + onChangeLegendRowCollapsed = EMPTY_FN, + layerOrder, + title, + position = 'bottom-right', + maxZoom = 21, + minZoom = 0 +} = {}) { + const rootSx = { + ...styles[position], + position: 'absolute', + minWidth: collapsed ? undefined : 240, + maxHeight: 'calc(100% - 120px)', + // height: collapsed ? undefined : '100%', + background: '#fafafa', + display: 'flex', + flexDirection: 'column' + }; + + return ( + + {collapsed ? ( + onChangeCollapsed(false)}> + + + ) : ( + + + {title} + + onChangeCollapsed(true)}> + + + + )} + + + {layers.map((l) => ( + + ))} + + + + ); +} + +NewLegendWidgetUI.defaultProps = { + layers: [], + customLegendTypes: {}, + collapsed: true, + title: 'Layers', + position: 'bottom-right' +}; + +NewLegendWidgetUI.propTypes = { + customLegendTypes: PropTypes.objectOf(PropTypes.func), + layers: PropTypes.array, + collapsed: PropTypes.bool.isRequired, + onChangeCollapsed: PropTypes.func.isRequired, + onChangeLegendRowCollapsed: PropTypes.func.isRequired, + onChangeVisibility: PropTypes.func.isRequired, + onChangeOpacity: PropTypes.func.isRequired, + layerOrder: PropTypes.arrayOf(PropTypes.string), + title: PropTypes.string, + position: PropTypes.oneOf(['top-left', 'top-right', 'bottom-left', 'bottom-right']) +}; + +export default NewLegendWidgetUI; + +/** + * Receives configuration options, send change events and renders a legend item + * @param {object} props + * @param {import('../legend/LegendWidgetUI').LegendData} props.layer - Layer object from redux store. + * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} props.onChangeCollapsed - Callback function for layer visibility change. + * @param {({ id, opacity }: { id: string, opacity: number }) => void} props.onChangeOpacity - Callback function for layer opacity change. + * @param {({ id, visible }: { id: string, visible: boolean }) => void} props.onChangeVisibility - Callback function for layer collapsed state change. + * @param {number} [props.maxZoom] - Global maximum zoom level for the map. + * @param {number} [props.minZoom] - Global minimum zoom level for the map. + * @returns {React.ReactNode} + */ +export function LegendItem({ + layer = EMPTY_OBJ, + onChangeCollapsed, + onChangeOpacity, + onChangeVisibility, + maxZoom, + minZoom +}) { + // layer legend defaults as defined here: https://docs.carto.com/carto-for-developers/carto-for-react/library-reference/widgets#legendwidget + const id = layer?.id; + const type = layer?.legend?.type; + const visible = layer?.visible ?? true; + const switchable = layer.switchable ?? true; + const collapsed = layer.collapsed ?? false; + const collapsible = layer.collapsible ?? true; + const opacity = layer.opacity ?? 1; + const showOpacityControl = layer.showOpacityControl ?? true; + + const isExpanded = visible && !collapsed; + const collapseIcon = isExpanded ? : ; + + const [opacityOpen, setOpacityOpen] = useState(false); + const menuAnchorRef = useRef(null); + const layerHasZoom = layer?.minZoom !== undefined || layer?.maxZoom !== undefined; + const showZoomNote = + layerHasZoom && (layer.minZoom > minZoom || layer.maxZoom < maxZoom); + + if (!layer) { + return null; + } + + return ( + `1px solid ${theme.palette.divider}` + } + }} + > + + {collapsible && ( + onChangeCollapsed({ id, collapsed: !collapsed })} + > + {collapseIcon} + + )} + + + {layer.title} + + {showZoomNote && ( + + Zoom level: {layer.minZoom} - {layer.maxZoom} + + )} + + {showOpacityControl && ( + onChangeOpacity({ id, opacity })} + /> + )} + {switchable && ( + + onChangeVisibility({ + id, + collapsed: visible ? collapsed : false, + visible: !visible + }) + } + > + {visible ? : } + + )} + + + {type === LEGEND_TYPES.CATEGORY && ( + + )} + {type === LEGEND_TYPES.ICON && } + {type === LEGEND_TYPES.BINS && } + + + ); +} + +/** + * @param {object} props + * @param {number} props.opacity - Opacity value + * @param {(opacity: number) => void} props.onChange - Callback function for opacity change + * @param {React.MutableRefObject} props.menuRef - Ref object for the menu anchor + * @param {boolean} props.open - Open state of the popover + * @param {(open: boolean) => void} props.toggleOpen - Callback function for open state change + * @returns {React.ReactNode} + */ +function OpacityControl({ menuRef, open, toggleOpen, opacity, onChange }) { + function handleTextFieldChange(e) { + const newOpacity = parseInt(e.target.value || '0'); + const clamped = clamp(newOpacity, 0, 100); + onChange(clamped / 100); + } + + return ( + <> + toggleOpen(!open)} + > + + + + + toggleOpen(false)} + anchorEl={menuRef.current} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'center' + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'center' + }} + sx={{ marginTop: -2 }} + > + + onChange(v / 100)} + min={0} + max={100} + step={1} + /> + + {' '} + % + + ) + }} + /> + + + + ); +} diff --git a/packages/react-ui/storybook/.storybook/preview.js b/packages/react-ui/storybook/.storybook/preview.js index 93888c561..9e26a36f7 100644 --- a/packages/react-ui/storybook/.storybook/preview.js +++ b/packages/react-ui/storybook/.storybook/preview.js @@ -1,14 +1,17 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { withDesign } from 'storybook-addon-designs'; -import { - createTheme, - responsiveFontSizes, - ThemeProvider, - StyledEngineProvider, - CssBaseline -} from '@mui/material'; -import { cartoThemeOptions, theme } from '../../src/theme/carto-theme'; +import { ThemeProvider, StyledEngineProvider, CssBaseline } from '@mui/material'; +import { theme } from '../../src/theme/carto-theme'; import { BREAKPOINTS } from '../../src/theme/themeConstants'; +import { + Title, + Subtitle, + Primary, + ArgsTable, + Stories, + PRIMARY_STORY, + DocsContext +} from '@storybook/addon-docs'; const customViewports = { xs: { @@ -89,13 +92,34 @@ export const decorators = [ ) ]; +function CustomDescription() { + const context = useContext(DocsContext); + try { + const jsdoc = context.parameters.docs.extractComponentDescription(context.component); + const excerpt = jsdoc?.split('@param')[0]; + return excerpt || null; + } catch { + return null; + } +} + export const parameters = { actions: { argTypesRegex: '^on[A-Z].*' }, viewMode: 'docs', docs: { source: { type: 'code' - } + }, + page: () => ( + <> + + <Subtitle /> + <CustomDescription /> + <Primary /> + <ArgsTable story={PRIMARY_STORY} /> + <Stories /> + </> + ) }, options: { storySort: { diff --git a/packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js b/packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js new file mode 100644 index 000000000..7ec6bc72b --- /dev/null +++ b/packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js @@ -0,0 +1,97 @@ +import React, { useReducer } from 'react'; +import LegendWidgetUI from '../../../src/widgets/new-legend/LegendWidgetUI'; +import { IntlProvider } from 'react-intl'; +import { Box } from '@mui/material'; +import { fixtures } from './legendFixtures'; + +const options = { + title: 'Widgets/NewLegendWidgetUI', + component: LegendWidgetUI, + argTypes: { + collapsed: { + defaultValue: true, + control: { + type: 'boolean' + } + }, + layers: { + defaultValue: fixtures, + control: { + type: 'array' + } + }, + layerOrder: { + defaultValue: [], + control: { + type: 'array' + } + }, + position: { + defaultValue: 'top-left', + options: ['bottom-left', 'bottom-right', 'top-left', 'top-right'], + control: { + type: 'radio' + } + } + }, + parameters: { + docs: { + source: { + type: 'auto' + } + } + } +}; +export default options; + +/** + * @param {Parameters<LegendWidgetUI>[0] & { height: number }} args + */ +const Widget = ({ height, ...props }) => ( + <IntlProvider locale='en'> + <Box sx={{ height, position: 'relative' }}> + <LegendWidgetUI {...props} /> + </Box> + </IntlProvider> +); + +function useLegendState(args) { + const [collapsed, setCollapsed] = React.useState(args.collapsed); + const [layers, dispatch] = useReducer((state, action) => { + switch (action.type) { + case 'add': + return [...state, action.layer]; + case 'remove': + return state.filter((layer) => layer.id !== action.layer.id); + case 'update': + return state.map((layer) => { + if (layer.id === action.layer.id) { + return { ...layer, ...action.layer }; + } + return layer; + }); + default: + throw new Error(`Unknown action type: ${action.type}`); + } + }, args.layers); + + return { collapsed, setCollapsed, layers, dispatch }; +} + +const Template = ({ ...args }) => { + const { collapsed, setCollapsed, layers, dispatch } = useLegendState(args); + + return ( + <Widget + {...args} + height={400} + layers={layers} + collapsed={collapsed} + onChangeCollapsed={setCollapsed} + onChangeLegendRowCollapsed={(layer) => dispatch({ type: 'update', layer })} + onChangeOpacity={(layer) => dispatch({ type: 'update', layer })} + onChangeVisibility={(layer) => dispatch({ type: 'update', layer })} + /> + ); +}; +export const Playground = Template.bind({}); diff --git a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js new file mode 100644 index 000000000..b7717c579 --- /dev/null +++ b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js @@ -0,0 +1,186 @@ +import { LEGEND_TYPES } from '@carto/react-ui'; + +export const fixtures = [ + { + id: 'applicants', + title: 'Applicants', + visible: false, + switchable: true, + opacity: 0.4, + showOpacityControl: true, + minZoom: 4, + maxZoom: 9, + legend: { + collapsed: false, + type: LEGEND_TYPES.ICON, + icons: [ + `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"> + <rect width="24" height="24" fill="#dd3741" rx="4" /> + </svg>` + ].map((txt) => txt.replace(/#/g, '%23')), + labels: ['Applicants'] + } + }, + { + id: 'avland', + title: 'Available Land', + visible: true, + switchable: true, + legend: { + collapsed: false, + type: LEGEND_TYPES.ICON, + icons: [ + `data:image/svg+xml,<svg width="26" height="42" view-box="0 0 26 42" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M24.9898 13.5C24.9966 13.3342 25 13.1675 25 13C25 6.37258 19.6274 1 13 1C6.37258 1 1 6.37258 1 13C1 13.1675 1.00343 13.3342 1.01023 13.5H1C1 15 1.66667 17.1667 2 18L13 39.5L24 18C24.7689 16.4972 25.0079 14.2072 25 13.5H24.9898Z" fill="#D40511"/> + <path d="M24.9898 13.5L23.9906 13.4591L23.948 14.5H24.9898V13.5ZM1.01023 13.5V14.5H2.05205L2.00939 13.459L1.01023 13.5ZM1 13.5V12.5H0V13.5H1ZM2 18L1.07152 18.3714L1.08869 18.4143L1.10975 18.4555L2 18ZM13 39.5L12.1098 39.9555L13 41.6955L13.8902 39.9555L13 39.5ZM24 18L23.1098 17.5445V17.5445L24 18ZM25 13.5L25.9999 13.4889L25.9889 12.5H25V13.5ZM24 13C24 13.1538 23.9968 13.3069 23.9906 13.4591L25.9889 13.5409C25.9963 13.3615 26 13.1811 26 13H24ZM13 2C19.0751 2 24 6.92487 24 13H26C26 5.8203 20.1797 0 13 0V2ZM2 13C2 6.92487 6.92487 2 13 2V0C5.8203 0 0 5.8203 0 13H2ZM2.00939 13.459C2.00315 13.3069 2 13.1538 2 13H0C0 13.1811 0.00371128 13.3615 0.0110667 13.541L2.00939 13.459ZM1 14.5H1.01023V12.5H1V14.5ZM2.92848 17.6286C2.78084 17.2595 2.54409 16.5532 2.34514 15.7575C2.14373 14.9518 2 14.1282 2 13.5H0C0 14.3718 0.189607 15.3815 0.404858 16.2425C0.622579 17.1134 0.885825 17.9071 1.07152 18.3714L2.92848 17.6286ZM13.8902 39.0445L2.89025 17.5445L1.10975 18.4555L12.1098 39.9555L13.8902 39.0445ZM23.1098 17.5445L12.1098 39.0445L13.8902 39.9555L24.8902 18.4555L23.1098 17.5445ZM24.0001 13.5111C24.0031 13.7838 23.9544 14.4669 23.8075 15.2721C23.6602 16.0796 23.43 16.9186 23.1098 17.5445L24.8902 18.4555C25.3389 17.5786 25.6126 16.5212 25.775 15.6311C25.9379 14.7388 26.0048 13.9234 25.9999 13.4889L24.0001 13.5111ZM24.9898 14.5H25V12.5H24.9898V14.5Z" fill="white"/> + <circle id="Ellipse 10" cx="13" cy="13" r="3" fill="white" stroke="white" stroke-width="2"/> + </svg>` + ].map((txt) => txt.replace(/#/g, '%23')), + colors: ['#D40511'], + labels: ['Available land'] + } + }, + { + id: 'catchment', + title: 'Catchment Area', + visible: true, + switchable: true, + legend: { + collapsed: false, + type: LEGEND_TYPES.CATEGORY, + colors: [`#FC483F33`], + labels: ['Catchment Area'] + } + }, + { + id: 'cust-loc', + title: 'Customer Locations', + visible: false, + switchable: true, + legend: { + collapsed: false, + type: LEGEND_TYPES.CATEGORY, + colors: ['#C8C8C8'], + labels: ['Others'] + } + }, + { + id: 'employees', + title: 'Employees', + visible: false, + switchable: true, + opacity: 0.4, + showOpacityControl: true, + legend: { + collapsed: false, + type: LEGEND_TYPES.ICON, + icons: [ + `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"> + <rect width="24" height="24" fill="#ffd633" rx="4" /> + </svg>` + ].map((txt) => txt.replace(/#/g, '%23')), + labels: ['Employees'] + } + }, + { + id: 'existing-ops', + title: 'DSC Existing Operations', + visible: true, + switchable: true, + legend: { + collapsed: false, + type: LEGEND_TYPES.ICON, + icons: [ + `data:image/svg+xml,<svg width="154" height="154" viewBox="0 0 154 154" fill="none" xmlns="http://www.w3.org/2000/svg"> + <circle opacity="0.5" cx="76.7998" cy="76.8" r="64" fill="white"/> + <circle cx="76.7996" cy="76.8001" r="51.2" fill="#FFCC00"/> + <circle cx="76.8002" cy="76.7999" r="25.6" fill="#D40511"/> + </svg>` + ].map((txt) => txt.replace(/#/g, '%23')), + labels: ['DSC Existing Operations'] + } + }, + { + id: 'spatial-index', + title: 'Spatial Index', + visible: true, + opacity: 100 / 255, + switchable: true, + showOpacityControl: true, + legend: { + collapsed: false, + type: LEGEND_TYPES.BINS, + labels: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 1], + colors: [ + '#ffffe5', + '#f7fcb9', + '#d9f0a3', + '#addd8e', + '#78c679', + '#41ab5d', + '#238443', + '#006837', + '#004529', + '#003529' + ] + } + }, + { + id: 'intermodal-points', + title: 'Intermodal points', + visible: false, + switchable: true, + legend: { + collapsed: false, + type: LEGEND_TYPES.ICON, + icons: [ + `data:image/svg+xml,<svg width="128" height="129" viewBox="0 0 128 129" fill="none" xmlns="http://www.w3.org/2000/svg"> + <circle opacity="0.5" cx="64.0003" cy="64.2" r="53.3333" fill="white"/> + <circle cx="63.9997" cy="64.1999" r="42.6667" fill="#0081F4"/> + <mask id="mask0_1694_41838" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="32" y="32" width="64" height="65"> + <rect x="32" y="32.2" width="64" height="64" fill="#D9D9D9"/> + </mask> + <g mask="url(#mask0_1694_41838)"> + <path d="M54.3955 89.8011V86.6011L59.9955 82.4011V67.6011L38.3955 73.8011V69.0011L59.9955 56.6677V42.6011C59.9955 41.49 60.3844 40.5455 61.1622 39.7677C61.94 38.99 62.8844 38.6011 63.9955 38.6011C65.1066 38.6011 66.0511 38.99 66.8288 39.7677C67.6066 40.5455 67.9955 41.49 67.9955 42.6011V56.6677L89.5955 69.0011V73.8011L67.9955 67.6011V82.4011L73.5955 86.6011V89.8011L63.9955 86.6011L54.3955 89.8011Z" fill="white"/> + </g> + </svg>`, + `data:image/svg+xml,<svg width="128" height="129" viewBox="0 0 128 129" fill="none" xmlns="http://www.w3.org/2000/svg"> + <circle opacity="0.5" cx="64.0003" cy="64.2" r="53.3333" fill="white"/> + <circle cx="63.9997" cy="64.1999" r="42.6667" fill="#0081F4"/> + <mask id="mask0_1700_40659" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="32" y="32" width="64" height="65"> + <rect x="32" y="32.2" width="64" height="64" fill="#D9D9D9"/> + </mask> + <g mask="url(#mask0_1700_40659)"> + <path d="M43.1982 86.5995C40.976 86.5995 39.0871 85.8217 37.5316 84.2662C35.976 82.7106 35.1982 80.8217 35.1982 78.5995C35.1982 77.3106 35.476 76.1106 36.0316 74.9995C36.5871 73.8884 37.376 72.9551 38.3982 72.1995V60.9995H44.7982V48.1995H63.9982L75.7316 73.5995C76.0871 74.2217 76.3538 74.8874 76.5316 75.5964C76.7094 76.3055 76.7982 77.0398 76.7982 77.7995C76.7982 80.23 75.9392 82.3043 74.2211 84.0224C72.503 85.7405 70.4287 86.5995 67.9982 86.5995C66.3251 86.5995 64.7846 86.1662 63.3767 85.2995C61.9688 84.4328 60.8775 83.2662 60.1027 81.7995H50.5316C49.9094 83.2662 48.9369 84.4328 47.6142 85.2995C46.2916 86.1662 44.8196 86.5995 43.1982 86.5995ZM79.9982 83.3995V44.9995H84.7982V78.5995H92.7982V83.3995H79.9982ZM43.1982 81.7995C44.1049 81.7995 44.8649 81.4929 45.4782 80.8795C46.0916 80.2662 46.3982 79.5062 46.3982 78.5995C46.3982 77.6928 46.0916 76.9328 45.4782 76.3195C44.8649 75.7062 44.1049 75.3995 43.1982 75.3995C42.2916 75.3995 41.5316 75.7062 40.9182 76.3195C40.3049 76.9328 39.9982 77.6928 39.9982 78.5995C39.9982 79.5062 40.3049 80.2662 40.9182 80.8795C41.5316 81.4929 42.2916 81.7995 43.1982 81.7995ZM67.9982 81.7995C69.1094 81.7995 70.0538 81.4106 70.8316 80.6328C71.6094 79.8551 71.9982 78.9106 71.9982 77.7995C71.9982 76.6884 71.6094 75.744 70.8316 74.9662C70.0538 74.1884 69.1094 73.7995 67.9982 73.7995C66.8871 73.7995 65.9427 74.1884 65.1649 74.9662C64.3871 75.744 63.9982 76.6884 63.9982 77.7995C63.9982 78.9106 64.3871 79.8551 65.1649 80.6328C65.9427 81.4106 66.8871 81.7995 67.9982 81.7995ZM56.3316 68.9995H68.3316L60.9316 52.9995H49.5982V62.5328L56.3316 68.9995Z" fill="white"/> + </g> + </svg>`, + `data:image/svg+xml,<svg width="128" height="129" viewBox="0 0 128 129" fill="none" xmlns="http://www.w3.org/2000/svg"> + <circle opacity="0.5" cx="64.0003" cy="64.2" r="53.3333" fill="white"/> + <circle cx="63.9997" cy="64.1999" r="42.6667" fill="#0081F4"/> + <mask id="mask0_1694_41840" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="32" y="32" width="64" height="65"> + <rect x="32" y="32.2" width="64" height="64" fill="#D9D9D9"/> + </mask> + <g mask="url(#mask0_1694_41840)"> + <path d="M43.1987 83.2671L38.5987 66.3337C38.2431 65.0893 38.3542 63.9115 38.932 62.8004C39.5098 61.6893 40.3987 60.9115 41.5987 60.4671L44.7987 59.3337V46.6004C44.7987 45.2671 45.2653 44.1337 46.1987 43.2004C47.132 42.2671 48.2653 41.8004 49.5987 41.8004H57.5987V35.4004H70.3987V41.8004H78.3987C79.732 41.8004 80.8653 42.2671 81.7987 43.2004C82.732 44.1337 83.1987 45.2671 83.1987 46.6004V59.3337L86.3987 60.4671C87.5542 60.8226 88.432 61.5671 89.032 62.7004C89.632 63.8337 89.7542 65.0448 89.3987 66.3337L84.7987 83.3337C83.4653 83.2448 82.0987 82.9004 80.6987 82.3004C79.2987 81.7004 77.4653 80.6671 75.1987 79.2004C73.332 80.6226 71.4653 81.6782 69.5987 82.3671C67.732 83.0559 65.8653 83.4004 63.9987 83.4004C62.132 83.4004 60.2653 83.0559 58.3987 82.3671C56.532 81.6782 54.6653 80.6226 52.7987 79.2004C50.6209 80.6226 48.8098 81.6337 47.3653 82.2337C45.9209 82.8337 44.532 83.1782 43.1987 83.2671ZM38.3987 93.0004V88.2004H41.5987C43.732 88.2004 45.7431 87.9226 47.632 87.3671C49.5209 86.8115 51.2431 86.0004 52.7987 84.9337C54.3098 86.0004 56.0098 86.8115 57.8987 87.3671C59.7876 87.9226 61.8209 88.2004 63.9987 88.2004C66.132 88.2004 68.1431 87.9226 70.032 87.3671C71.9209 86.8115 73.6431 86.0004 75.1987 84.9337C76.7542 86.0004 78.4765 86.8115 80.3653 87.3671C82.2542 87.9226 84.2653 88.2004 86.3987 88.2004H89.5987V93.0004H86.3987C84.2653 93.0004 82.2542 92.7893 80.3653 92.3671C78.4765 91.9448 76.7542 91.2893 75.1987 90.4004C73.6431 91.2893 71.9209 91.9448 70.032 92.3671C68.1431 92.7893 66.132 93.0004 63.9987 93.0004C61.8209 93.0004 59.7987 92.7893 57.932 92.3671C56.0653 91.9448 54.3542 91.2893 52.7987 90.4004C51.2431 91.2893 49.5209 91.9448 47.632 92.3671C45.7431 92.7893 43.732 93.0004 41.5987 93.0004H38.3987ZM49.5987 57.5337L63.9987 52.3337L78.3987 57.6004V46.6004H49.5987V57.5337Z" fill="white"/> + </g> + </svg>`, + `data:image/svg+xml,<svg width="128" height="129" viewBox="0 0 128 129" fill="none" xmlns="http://www.w3.org/2000/svg"> + <circle opacity="0.5" cx="64.0003" cy="64.2" r="53.3333" fill="white"/> + <circle cx="63.9997" cy="64.1999" r="42.6667" fill="#0081F4"/> + <mask id="mask0_1694_41839" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="32" y="32" width="64" height="65"> + <rect x="32" y="32.2" width="64" height="64" fill="#D9D9D9"/> + </mask> + <g mask="url(#mask0_1694_41839)"> + <path d="M49.6018 89.7988V88.1988L52.9351 84.8655C50.5795 84.4655 48.6351 83.3766 47.1018 81.5988C45.5684 79.8211 44.8018 77.7544 44.8018 75.3988V51.3988C44.8018 48.1544 46.2906 45.7433 49.2684 44.1655C52.2462 42.5877 57.1573 41.7988 64.0018 41.7988C70.8462 41.7988 75.7573 42.5877 78.7351 44.1655C81.7129 45.7433 83.2018 48.1544 83.2018 51.3988V75.3988C83.2018 77.7544 82.4351 79.8211 80.9018 81.5988C79.3684 83.3766 77.424 84.4655 75.0684 84.8655L78.4018 88.1988V89.7988H49.6018ZM49.6018 62.5988H61.6018V54.5988H49.6018V62.5988ZM66.4018 62.5988H78.4018V54.5988H66.4018V62.5988ZM56.0018 76.9988C56.8906 76.9988 57.6462 76.6877 58.2684 76.0655C58.8906 75.4433 59.2018 74.6877 59.2018 73.7988C59.2018 72.9099 58.8906 72.1544 58.2684 71.5322C57.6462 70.9099 56.8906 70.5988 56.0018 70.5988C55.1129 70.5988 54.3573 70.9099 53.7351 71.5322C53.1129 72.1544 52.8018 72.9099 52.8018 73.7988C52.8018 74.6877 53.1129 75.4433 53.7351 76.0655C54.3573 76.6877 55.1129 76.9988 56.0018 76.9988ZM72.0018 76.9988C72.8906 76.9988 73.6462 76.6877 74.2684 76.0655C74.8906 75.4433 75.2018 74.6877 75.2018 73.7988C75.2018 72.9099 74.8906 72.1544 74.2684 71.5322C73.6462 70.9099 72.8906 70.5988 72.0018 70.5988C71.1129 70.5988 70.3573 70.9099 69.7351 71.5322C69.1129 72.1544 68.8018 72.9099 68.8018 73.7988C68.8018 74.6877 69.1129 75.4433 69.7351 76.0655C70.3573 76.6877 71.1129 76.9988 72.0018 76.9988Z" fill="white"/> + </g> + </svg>`, + `data:image/svg+xml,<svg width="128" height="129" viewBox="0 0 128 129" fill="none" xmlns="http://www.w3.org/2000/svg"> + <circle opacity="0.5" cx="64.0003" cy="64.2" r="53.3333" fill="white"/> + <circle cx="63.9997" cy="64.1999" r="42.6667" fill="#A5AA99"/> + <circle cx="64.0003" cy="64.2" r="21.3333" fill="white"/> + </svg>` + ].map((txt) => txt.replace(/#/g, '%23')), + labels: ['Airport', 'Dry Port', 'Port', 'Rail Hub', 'Others'] + } + } +]; diff --git a/tsconfig.json b/tsconfig.json index 7c51700ce..fb6c3a177 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "strict": true, "noImplicitAny": false, "allowJs": true, - "checkJs": true, + "checkJs": false, "moduleResolution": "node", "esModuleInterop": true, "allowSyntheticDefaultImports": true, From b34565033d8cea69a83bf5d93f333562361437ef Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Fri, 2 Feb 2024 15:29:13 +0100 Subject: [PATCH 02/66] finish icon buttons with tooltips --- .../src/widgets/new-legend/LegendWidgetUI.js | 100 ++++++++++-------- 1 file changed, 57 insertions(+), 43 deletions(-) diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js index 092c0cb14..219875ae3 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js @@ -8,6 +8,7 @@ import { Popover, Slider, TextField, + Tooltip, Typography } from '@mui/material'; import { LEGEND_TYPES, LegendCategories, LegendIcon, LegendRamp } from '@carto/react-ui'; @@ -56,7 +57,8 @@ export const styles = { gap: 0.5, display: 'flex', justifyContent: 'space-between', - position: 'relative' + position: 'sticky', + top: 0 }, legendItemBody: { px: 2 @@ -85,7 +87,7 @@ export const styles = { gap: 2, alignItems: 'center', p: 1, - width: 208 + minWidth: 208 }, layerOptions: { background: (theme) => theme.palette.background.default, @@ -164,9 +166,11 @@ function NewLegendWidgetUI({ return ( <Paper elevation={3} sx={rootSx}> {collapsed ? ( - <IconButton onClick={() => onChangeCollapsed(false)}> - <LayerIcon /> - </IconButton> + <Tooltip title='Open legend'> + <IconButton onClick={() => onChangeCollapsed(false)}> + <LayerIcon /> + </IconButton> + </Tooltip> ) : ( <Box sx={{ @@ -177,9 +181,11 @@ function NewLegendWidgetUI({ <Typography variant='caption' sx={{ flexGrow: 1 }}> {title} </Typography> - <IconButton size='small' onClick={() => onChangeCollapsed(true)}> - <CloseIcon /> - </IconButton> + <Tooltip title='Close'> + <IconButton size='small' onClick={() => onChangeCollapsed(true)}> + <CloseIcon /> + </IconButton> + </Tooltip> </Box> )} <Box sx={{ ...styles.legendItemList, width: collapsed ? 0 : undefined }}> @@ -317,18 +323,20 @@ export function LegendItem({ /> )} {switchable && ( - <IconButton - size='small' - onClick={() => - onChangeVisibility({ - id, - collapsed: visible ? collapsed : false, - visible: !visible - }) - } - > - {visible ? <EyeIcon /> : <EyeOffIcon />} - </IconButton> + <Tooltip title={visible ? 'Hide layer' : 'Show layer'}> + <IconButton + size='small' + onClick={() => + onChangeVisibility({ + id, + collapsed: visible ? collapsed : false, + visible: !visible + }) + } + > + {visible ? <EyeIcon /> : <EyeOffIcon />} + </IconButton> + </Tooltip> )} </Box> <Collapse timeout={100} sx={styles.legendItemBody} in={isExpanded}> @@ -360,39 +368,45 @@ function OpacityControl({ menuRef, open, toggleOpen, opacity, onChange }) { return ( <> - <IconButton - size='small' - aria-label='Toggle opacity control' - color={open ? 'primary' : 'default'} - onClick={() => toggleOpen(!open)} - > - <svg - width='24' - height='24' - viewBox='0 0 24 24' - fill='none' - xmlns='http://www.w3.org/2000/svg' + <Tooltip title='Layer opacity'> + <IconButton + size='small' + aria-label='Toggle opacity control' + color={open ? 'primary' : 'default'} + onClick={() => toggleOpen(!open)} > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM17.625 19V17.625H19V15.375H17.625V13.125H19V10.875H17.625V8.625H19V6.375H17.625V5H15.375V6.375H13.125V5H10.875V6.375H8.625V5H6.375V6.375H5V8.625H6.375V10.875H5V13.125H6.375V15.375H5V17.625H6.375V19H8.625V17.625H10.875V19H13.125V17.625H15.375V19H17.625ZM15.375 15.375H17.625V17.625H15.375V15.375ZM13.125 15.375H15.375V13.125H17.625V10.875H15.375V8.625H17.625V6.375H15.375V8.625H13.125V6.375H10.875V8.625H8.625V6.375H6.375V8.625H8.625V10.875H6.375V13.125H8.625V15.375H6.375V17.625H8.625V15.375H10.875V17.625H13.125V15.375ZM13.125 13.125H15.375V10.875H13.125V8.625H10.875V10.875H8.625V13.125H10.875V15.375H13.125V13.125ZM13.125 13.125H10.875V10.875H13.125V13.125Z' - /> - </svg> - </IconButton> + <svg + width='24' + height='24' + viewBox='0 0 24 24' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <path + fillRule='evenodd' + clipRule='evenodd' + d='M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM17.625 19V17.625H19V15.375H17.625V13.125H19V10.875H17.625V8.625H19V6.375H17.625V5H15.375V6.375H13.125V5H10.875V6.375H8.625V5H6.375V6.375H5V8.625H6.375V10.875H5V13.125H6.375V15.375H5V17.625H6.375V19H8.625V17.625H10.875V19H13.125V17.625H15.375V19H17.625ZM15.375 15.375H17.625V17.625H15.375V15.375ZM13.125 15.375H15.375V13.125H17.625V10.875H15.375V8.625H17.625V6.375H15.375V8.625H13.125V6.375H10.875V8.625H8.625V6.375H6.375V8.625H8.625V10.875H6.375V13.125H8.625V15.375H6.375V17.625H8.625V15.375H10.875V17.625H13.125V15.375ZM13.125 13.125H15.375V10.875H13.125V8.625H10.875V10.875H8.625V13.125H10.875V15.375H13.125V13.125ZM13.125 13.125H10.875V10.875H13.125V13.125Z' + /> + </svg> + </IconButton> + </Tooltip> <Popover open={open} onClose={() => toggleOpen(false)} anchorEl={menuRef.current} anchorOrigin={{ - vertical: 'bottom', - horizontal: 'center' + vertical: 'top', + horizontal: 'right' }} transformOrigin={{ vertical: 'top', - horizontal: 'center' + horizontal: 'right' + }} + slotProps={{ + root: { + sx: { transform: 'translate(-12px, 36px)' } + } }} - sx={{ marginTop: -2 }} > <Box sx={styles.opacityControl}> <Slider From c264e1c0feb593b4f4c115008cdd68cc3b25596a Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Fri, 2 Feb 2024 16:48:40 +0100 Subject: [PATCH 03/66] legend helper text --- .../src/widgets/new-legend/LegendWidgetUI.js | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js index 219875ae3..8020f6373 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js @@ -230,6 +230,21 @@ NewLegendWidgetUI.propTypes = { export default NewLegendWidgetUI; +/** + * @param {object} props + * @param {number} props.minZoom - Global minimum zoom level for the map. + * @param {number} props.maxZoom - Global maximum zoom level for the map. + * @param {number} props.layerMinZoom - Layer minimum zoom level. + * @param {number} props.layerMaxZoom - Layer maximum zoom level. + * @returns {string} + */ +function getZoomHelperText({ minZoom, maxZoom, layerMinZoom, layerMaxZoom }) { + const maxZoomText = layerMaxZoom < maxZoom ? `lower than ${layerMaxZoom}` : ''; + const minZoomText = layerMinZoom > minZoom ? `greater than ${layerMinZoom}` : ''; + const texts = [maxZoomText, minZoomText].filter(Boolean).join(' and '); + return texts ? `Note: this layer will display at zoom levels ${texts}` : ''; +} + /** * Receives configuration options, send change events and renders a legend item * @param {object} props @@ -268,6 +283,14 @@ export function LegendItem({ const showZoomNote = layerHasZoom && (layer.minZoom > minZoom || layer.maxZoom < maxZoom); + const zoomHelperText = getZoomHelperText({ + minZoom, + maxZoom, + layerMinZoom: layer.minZoom, + layerMaxZoom: layer.maxZoom + }); + const helperText = layer.helperText ?? showZoomNote ? zoomHelperText : ''; + if (!layer) { return null; } @@ -340,11 +363,27 @@ export function LegendItem({ )} </Box> <Collapse timeout={100} sx={styles.legendItemBody} in={isExpanded}> - {type === LEGEND_TYPES.CATEGORY && ( - <LegendCategories layer={layer} legend={layer.legend} /> + <Box pb={2}> + {type === LEGEND_TYPES.CATEGORY && ( + <LegendCategories layer={layer} legend={layer.legend} /> + )} + {type === LEGEND_TYPES.ICON && ( + <LegendIcon layer={layer} legend={layer.legend} /> + )} + {type === LEGEND_TYPES.BINS && ( + <LegendRamp layer={layer} legend={layer.legend} /> + )} + </Box> + {helperText && ( + <Typography + variant='caption' + color='textSecondary' + component='p' + sx={{ py: 2 }} + > + {helperText} + </Typography> )} - {type === LEGEND_TYPES.ICON && <LegendIcon layer={layer} legend={layer.legend} />} - {type === LEGEND_TYPES.BINS && <LegendRamp layer={layer} legend={layer.legend} />} </Collapse> </Box> ); From 5250f5514d5a887a7c5a968cbf2fb21a409130dd Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Fri, 2 Feb 2024 17:01:00 +0100 Subject: [PATCH 04/66] extract legend width constant --- .../src/widgets/new-legend/LegendWidgetUI.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js index 8020f6373..063071ee7 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js @@ -20,6 +20,11 @@ import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import LayerIcon from '@mui/icons-material/LayersOutlined'; import { useRef, useState } from 'react'; +const EMPTY_OBJ = {}; +const EMPTY_FN = () => {}; +const EMPTY_ARR = []; +const LEGEND_WIDTH = 240; + /** * Returns a number whose value is limited to the given range. * @@ -87,7 +92,7 @@ export const styles = { gap: 2, alignItems: 'center', p: 1, - minWidth: 208 + minWidth: LEGEND_WIDTH - 32 }, layerOptions: { background: (theme) => theme.palette.background.default, @@ -118,10 +123,6 @@ export const styles = { } }; -const EMPTY_OBJ = {}; -const EMPTY_FN = () => {}; -const EMPTY_ARR = []; - /** * @param {object} props * @param {Object.<string, Function>} [props.customLegendTypes] - Allow to customise by default legend types that can be rendered. @@ -155,7 +156,7 @@ function NewLegendWidgetUI({ const rootSx = { ...styles[position], position: 'absolute', - minWidth: collapsed ? undefined : 240, + width: collapsed ? undefined : LEGEND_WIDTH, maxHeight: 'calc(100% - 120px)', // height: collapsed ? undefined : '100%', background: '#fafafa', @@ -315,13 +316,14 @@ export function LegendItem({ {collapseIcon} </IconButton> )} - <Box flexGrow={1}> + <Box flexGrow={1} sx={{ minWidth: 0, flexShrink: 1 }}> <Typography color={visible ? 'textPrimary' : 'textSecondary'} variant='button' fontWeight={500} lineHeight='20px' component='p' + noWrap sx={{ my: 0.25 }} > {layer.title} From 04994427fe74cbaece7fa78bb02b2f7733ec53aa Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Fri, 2 Feb 2024 17:14:41 +0100 Subject: [PATCH 05/66] conditional overflow tooltip component --- .../src/widgets/new-legend/LegendWidgetUI.js | 89 +++++++++++++------ 1 file changed, 62 insertions(+), 27 deletions(-) diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js index 063071ee7..e896d15f2 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js @@ -18,7 +18,7 @@ import CloseIcon from '@mui/icons-material/Close'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import LayerIcon from '@mui/icons-material/LayersOutlined'; -import { useRef, useState } from 'react'; +import { useLayoutEffect, useRef, useState } from 'react'; const EMPTY_OBJ = {}; const EMPTY_FN = () => {}; @@ -195,6 +195,7 @@ function NewLegendWidgetUI({ <LegendItem key={l.id} layer={l} + collapsed={collapsed} onChangeCollapsed={onChangeLegendRowCollapsed} onChangeOpacity={onChangeOpacity} onChangeVisibility={onChangeVisibility} @@ -231,21 +232,6 @@ NewLegendWidgetUI.propTypes = { export default NewLegendWidgetUI; -/** - * @param {object} props - * @param {number} props.minZoom - Global minimum zoom level for the map. - * @param {number} props.maxZoom - Global maximum zoom level for the map. - * @param {number} props.layerMinZoom - Layer minimum zoom level. - * @param {number} props.layerMaxZoom - Layer maximum zoom level. - * @returns {string} - */ -function getZoomHelperText({ minZoom, maxZoom, layerMinZoom, layerMaxZoom }) { - const maxZoomText = layerMaxZoom < maxZoom ? `lower than ${layerMaxZoom}` : ''; - const minZoomText = layerMinZoom > minZoom ? `greater than ${layerMinZoom}` : ''; - const texts = [maxZoomText, minZoomText].filter(Boolean).join(' and '); - return texts ? `Note: this layer will display at zoom levels ${texts}` : ''; -} - /** * Receives configuration options, send change events and renders a legend item * @param {object} props @@ -253,6 +239,7 @@ function getZoomHelperText({ minZoom, maxZoom, layerMinZoom, layerMaxZoom }) { * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} props.onChangeCollapsed - Callback function for layer visibility change. * @param {({ id, opacity }: { id: string, opacity: number }) => void} props.onChangeOpacity - Callback function for layer opacity change. * @param {({ id, visible }: { id: string, visible: boolean }) => void} props.onChangeVisibility - Callback function for layer collapsed state change. + * @param {boolean} [props.collapsed] - Collapsed state for the whole legend. * @param {number} [props.maxZoom] - Global maximum zoom level for the map. * @param {number} [props.minZoom] - Global minimum zoom level for the map. * @returns {React.ReactNode} @@ -262,6 +249,7 @@ export function LegendItem({ onChangeCollapsed, onChangeOpacity, onChangeVisibility, + collapsed: legendCollapsed, maxZoom, minZoom }) { @@ -317,17 +305,10 @@ export function LegendItem({ </IconButton> )} <Box flexGrow={1} sx={{ minWidth: 0, flexShrink: 1 }}> - <Typography - color={visible ? 'textPrimary' : 'textSecondary'} - variant='button' - fontWeight={500} - lineHeight='20px' - component='p' - noWrap - sx={{ my: 0.25 }} - > - {layer.title} - </Typography> + <LegendItemTitle + visible={legendCollapsed ? false : visible} + title={layer.title} + /> {showZoomNote && ( <Typography color={visible ? 'textPrimary' : 'textSecondary'} @@ -483,3 +464,57 @@ function OpacityControl({ menuRef, open, toggleOpen, opacity, onChange }) { </> ); } + +/** + * @param {object} props + * @param {number} props.minZoom - Global minimum zoom level for the map. + * @param {number} props.maxZoom - Global maximum zoom level for the map. + * @param {number} props.layerMinZoom - Layer minimum zoom level. + * @param {number} props.layerMaxZoom - Layer maximum zoom level. + * @returns {string} + */ +function getZoomHelperText({ minZoom, maxZoom, layerMinZoom, layerMaxZoom }) { + const maxZoomText = layerMaxZoom < maxZoom ? `lower than ${layerMaxZoom}` : ''; + const minZoomText = layerMinZoom > minZoom ? `greater than ${layerMinZoom}` : ''; + const texts = [maxZoomText, minZoomText].filter(Boolean).join(' and '); + return texts ? `Note: this layer will display at zoom levels ${texts}` : ''; +} + +/** + * @param {object} props + * @param {string} props.title + * @param {boolean} props.visible + * @returns {React.ReactNode} + */ +function LegendItemTitle({ title, visible }) { + const ref = useRef(null); + const [isOverflow, setIsOverflow] = useState(false); + + useLayoutEffect(() => { + if (visible && ref.current) { + const { offsetWidth, scrollWidth } = ref.current; + setIsOverflow(offsetWidth < scrollWidth); + } + }, [title, visible]); + + const element = ( + <Typography + ref={ref} + color={visible ? 'textPrimary' : 'textSecondary'} + variant='button' + fontWeight={500} + lineHeight='20px' + component='p' + noWrap + sx={{ my: 0.25 }} + > + {title} + </Typography> + ); + + if (!isOverflow) { + return element; + } + + return <Tooltip title={title}>{element}</Tooltip>; +} From 07268b093a6b9c558ecbabe10ee0bddd5a9cdbce Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Mon, 5 Feb 2024 10:54:56 +0100 Subject: [PATCH 06/66] sticky backgrounds --- .../src/widgets/new-legend/LegendWidgetUI.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js index e896d15f2..a05804164 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js @@ -36,6 +36,13 @@ const LEGEND_WIDTH = 240; const clamp = (val, min, max) => Math.min(Math.max(val, min), max); export const styles = { + root: { + background: (theme) => theme.palette.background.paper, + position: 'absolute', + maxHeight: 'calc(100% - 120px)', + display: 'flex', + flexDirection: 'column' + }, legendToggleOpen: { borderBottom: (theme) => `1px solid ${theme.palette.divider}` }, @@ -63,7 +70,8 @@ export const styles = { display: 'flex', justifyContent: 'space-between', position: 'sticky', - top: 0 + top: 0, + background: (theme) => theme.palette.background.paper }, legendItemBody: { px: 2 @@ -155,13 +163,8 @@ function NewLegendWidgetUI({ } = {}) { const rootSx = { ...styles[position], - position: 'absolute', - width: collapsed ? undefined : LEGEND_WIDTH, - maxHeight: 'calc(100% - 120px)', - // height: collapsed ? undefined : '100%', - background: '#fafafa', - display: 'flex', - flexDirection: 'column' + ...styles.root, + width: collapsed ? undefined : LEGEND_WIDTH }; return ( From 28af0752372a0943699a1038a34382ef7e1854ed Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Mon, 5 Feb 2024 12:14:18 +0100 Subject: [PATCH 07/66] remove unnecesary prop passing in favor of unmountOnExit --- .../src/widgets/new-legend/LegendWidgetUI.js | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js index a05804164..1e41eb01f 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js @@ -74,26 +74,26 @@ export const styles = { background: (theme) => theme.palette.background.paper }, legendItemBody: { - px: 2 - // '& [data-testid="categories-legend"] > .MuiGrid-root': { - // paddingTop: '6px', - // paddingBottom: '6px', - // }, - // '& [data-testid="icon-legend"] > .MuiGrid-root': { - // paddingTop: '2px', - // paddingBottom: '2px', - // '& > .MuiBox-root': { - // width: '20px', - // height: '20px', - // marginRight: '8px', - // }, - // '& img': { - // display: 'block', - // margin: 'auto', - // width: 'auto', - // height: '20px', - // }, - // }, + px: 2, + '& [data-testid="categories-legend"] > .MuiGrid-root': { + paddingTop: '6px', + paddingBottom: '6px' + }, + '& [data-testid="icon-legend"] > .MuiGrid-root': { + paddingTop: '2px', + paddingBottom: '2px', + '& > .MuiBox-root': { + width: '20px', + height: '20px', + marginRight: '8px' + }, + '& img': { + display: 'block', + margin: 'auto', + width: 'auto', + height: '20px' + } + } }, opacityControl: { display: 'flex', @@ -145,6 +145,7 @@ export const styles = { * @param {'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'} [props.position] - Position of the widget. * @param {number} [props.maxZoom] - Global maximum zoom level for the map. * @param {number} [props.minZoom] - Global minimum zoom level for the map. + * @param {number} [props.currentZoom] - Current zoom level for the map. * @returns {React.ReactNode} */ function NewLegendWidgetUI({ @@ -159,7 +160,8 @@ function NewLegendWidgetUI({ title, position = 'bottom-right', maxZoom = 21, - minZoom = 0 + minZoom = 0, + currentZoom } = {}) { const rootSx = { ...styles[position], @@ -193,17 +195,17 @@ function NewLegendWidgetUI({ </Box> )} <Box sx={{ ...styles.legendItemList, width: collapsed ? 0 : undefined }}> - <Collapse in={!collapsed} timeout={500}> + <Collapse unmountOnExit in={!collapsed} timeout={500}> {layers.map((l) => ( <LegendItem key={l.id} layer={l} - collapsed={collapsed} onChangeCollapsed={onChangeLegendRowCollapsed} onChangeOpacity={onChangeOpacity} onChangeVisibility={onChangeVisibility} maxZoom={maxZoom} minZoom={minZoom} + currentZoom={currentZoom} /> ))} </Collapse> @@ -242,9 +244,9 @@ export default NewLegendWidgetUI; * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} props.onChangeCollapsed - Callback function for layer visibility change. * @param {({ id, opacity }: { id: string, opacity: number }) => void} props.onChangeOpacity - Callback function for layer opacity change. * @param {({ id, visible }: { id: string, visible: boolean }) => void} props.onChangeVisibility - Callback function for layer collapsed state change. - * @param {boolean} [props.collapsed] - Collapsed state for the whole legend. * @param {number} [props.maxZoom] - Global maximum zoom level for the map. * @param {number} [props.minZoom] - Global minimum zoom level for the map. + * @param {number} [props.currentZoom] - Current zoom level for the map. * @returns {React.ReactNode} */ export function LegendItem({ @@ -252,9 +254,9 @@ export function LegendItem({ onChangeCollapsed, onChangeOpacity, onChangeVisibility, - collapsed: legendCollapsed, maxZoom, - minZoom + minZoom, + currentZoom }) { // layer legend defaults as defined here: https://docs.carto.com/carto-for-developers/carto-for-react/library-reference/widgets#legendwidget const id = layer?.id; @@ -274,6 +276,7 @@ export function LegendItem({ const layerHasZoom = layer?.minZoom !== undefined || layer?.maxZoom !== undefined; const showZoomNote = layerHasZoom && (layer.minZoom > minZoom || layer.maxZoom < maxZoom); + const outsideCurrentZoom = currentZoom < layer.minZoom || currentZoom > layer.maxZoom; const zoomHelperText = getZoomHelperText({ minZoom, @@ -308,10 +311,7 @@ export function LegendItem({ </IconButton> )} <Box flexGrow={1} sx={{ minWidth: 0, flexShrink: 1 }}> - <LegendItemTitle - visible={legendCollapsed ? false : visible} - title={layer.title} - /> + <LegendItemTitle visible={visible} title={layer.title} /> {showZoomNote && ( <Typography color={visible ? 'textPrimary' : 'textSecondary'} @@ -348,8 +348,8 @@ export function LegendItem({ </Tooltip> )} </Box> - <Collapse timeout={100} sx={styles.legendItemBody} in={isExpanded}> - <Box pb={2}> + <Collapse unmountOnExit timeout={100} sx={styles.legendItemBody} in={isExpanded}> + <Box pb={2} opacity={outsideCurrentZoom ? 0.5 : 1}> {type === LEGEND_TYPES.CATEGORY && ( <LegendCategories layer={layer} legend={layer.legend} /> )} From 6f0b7c6a7ef900feada24f72c48f66e59db95ec7 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Mon, 5 Feb 2024 12:28:18 +0100 Subject: [PATCH 08/66] split new legend into multiple files --- .../src/widgets/new-legend/LegendItem.js | 332 ++++++++++++++ .../src/widgets/new-legend/LegendWidgetUI.js | 413 +----------------- .../new-legend/LegendWidgetUI.styles.js | 96 ++++ .../widgetsUI/NewLegendWidgetUI.stories.js | 1 + 4 files changed, 432 insertions(+), 410 deletions(-) create mode 100644 packages/react-ui/src/widgets/new-legend/LegendItem.js create mode 100644 packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js diff --git a/packages/react-ui/src/widgets/new-legend/LegendItem.js b/packages/react-ui/src/widgets/new-legend/LegendItem.js new file mode 100644 index 000000000..12b12b3a2 --- /dev/null +++ b/packages/react-ui/src/widgets/new-legend/LegendItem.js @@ -0,0 +1,332 @@ +import PropTypes from 'prop-types'; +import { + Box, + Collapse, + IconButton, + InputAdornment, + Popover, + Slider, + TextField, + Tooltip, + Typography +} from '@mui/material'; +import { LEGEND_TYPES, LegendCategories, LegendIcon, LegendRamp } from '@carto/react-ui'; +import EyeIcon from '@mui/icons-material/VisibilityOutlined'; +import EyeOffIcon from '@mui/icons-material/VisibilityOffOutlined'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import { useLayoutEffect, useRef, useState } from 'react'; +import { styles } from './LegendWidgetUI.styles'; + +const EMPTY_OBJ = {}; + +/** + * Receives configuration options, send change events and renders a legend item + * @param {object} props + * @param {import('../legend/LegendWidgetUI').LegendData} props.layer - Layer object from redux store. + * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} props.onChangeCollapsed - Callback function for layer visibility change. + * @param {({ id, opacity }: { id: string, opacity: number }) => void} props.onChangeOpacity - Callback function for layer opacity change. + * @param {({ id, visible }: { id: string, visible: boolean }) => void} props.onChangeVisibility - Callback function for layer collapsed state change. + * @param {number} props.maxZoom - Global maximum zoom level for the map. + * @param {number} props.minZoom - Global minimum zoom level for the map. + * @param {number} props.currentZoom - Current zoom level for the map. + * @returns {React.ReactNode} + */ +export default function LegendItem({ + layer = EMPTY_OBJ, + onChangeCollapsed, + onChangeOpacity, + onChangeVisibility, + maxZoom, + minZoom, + currentZoom +}) { + // layer legend defaults as defined here: https://docs.carto.com/carto-for-developers/carto-for-react/library-reference/widgets#legendwidget + const id = layer?.id; + const type = layer?.legend?.type; + const visible = layer?.visible ?? true; + const switchable = layer.switchable ?? true; + const collapsed = layer.collapsed ?? false; + const collapsible = layer.collapsible ?? true; + const opacity = layer.opacity ?? 1; + const showOpacityControl = layer.showOpacityControl ?? true; + + const isExpanded = visible && !collapsed; + const collapseIcon = isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />; + + const [opacityOpen, setOpacityOpen] = useState(false); + const menuAnchorRef = useRef(null); + const layerHasZoom = layer?.minZoom !== undefined || layer?.maxZoom !== undefined; + const showZoomNote = + layerHasZoom && (layer.minZoom > minZoom || layer.maxZoom < maxZoom); + const outsideCurrentZoom = currentZoom < layer.minZoom || currentZoom > layer.maxZoom; + + const zoomHelperText = getZoomHelperText({ + minZoom, + maxZoom, + layerMinZoom: layer.minZoom, + layerMaxZoom: layer.maxZoom + }); + const helperText = layer.helperText ?? showZoomNote ? zoomHelperText : ''; + + if (!layer) { + return null; + } + + return ( + <Box + component='section' + sx={{ + '&:not(:first-of-type)': { + borderTop: (theme) => `1px solid ${theme.palette.divider}` + } + }} + > + <Box ref={menuAnchorRef} component='header' sx={styles.legendItemHeader}> + {collapsible && ( + <IconButton + size='small' + aria-label='Toggle legend item collapsed' + disabled={!visible} + onClick={() => onChangeCollapsed({ id, collapsed: !collapsed })} + > + {collapseIcon} + </IconButton> + )} + <Box flexGrow={1} sx={{ minWidth: 0, flexShrink: 1 }}> + <LegendItemTitle visible={visible} title={layer.title} /> + {showZoomNote && ( + <Typography + color={visible ? 'textPrimary' : 'textSecondary'} + variant='caption' + component='p' + > + Zoom level: {layer.minZoom} - {layer.maxZoom} + </Typography> + )} + </Box> + {showOpacityControl && ( + <OpacityControl + menuRef={menuAnchorRef} + open={opacityOpen} + toggleOpen={setOpacityOpen} + opacity={opacity} + onChange={(opacity) => onChangeOpacity({ id, opacity })} + /> + )} + {switchable && ( + <Tooltip title={visible ? 'Hide layer' : 'Show layer'}> + <IconButton + size='small' + onClick={() => + onChangeVisibility({ + id, + collapsed: visible ? collapsed : false, + visible: !visible + }) + } + > + {visible ? <EyeIcon /> : <EyeOffIcon />} + </IconButton> + </Tooltip> + )} + </Box> + <Collapse unmountOnExit timeout={100} sx={styles.legendItemBody} in={isExpanded}> + <Box pb={2} opacity={outsideCurrentZoom ? 0.5 : 1}> + {type === LEGEND_TYPES.CATEGORY && ( + <LegendCategories layer={layer} legend={layer.legend} /> + )} + {type === LEGEND_TYPES.ICON && ( + <LegendIcon layer={layer} legend={layer.legend} /> + )} + {type === LEGEND_TYPES.BINS && ( + <LegendRamp layer={layer} legend={layer.legend} /> + )} + </Box> + {helperText && ( + <Typography + variant='caption' + color='textSecondary' + component='p' + sx={{ py: 2 }} + > + {helperText} + </Typography> + )} + </Collapse> + </Box> + ); +} + +LegendItem.propTypes = { + layer: PropTypes.object.isRequired, + onChangeCollapsed: PropTypes.func.isRequired, + onChangeOpacity: PropTypes.func.isRequired, + onChangeVisibility: PropTypes.func.isRequired, + maxZoom: PropTypes.number, + minZoom: PropTypes.number, + currentZoom: PropTypes.number +}; +LegendItem.defaultProps = { + maxZoom: 21, + minZoom: 0, + currentZoom: 0 +}; + +/** + * Returns a number whose value is limited to the given range. + * @param {Number} val The initial value + * @param {Number} min The lower boundary + * @param {Number} max The upper boundary + * @returns {Number} A number in the range (min, max) + */ +function clamp(val, min, max) { + return Math.min(Math.max(val, min), max); +} + +/** + * @param {object} props + * @param {number} props.opacity - Opacity value + * @param {(opacity: number) => void} props.onChange - Callback function for opacity change + * @param {React.MutableRefObject} props.menuRef - Ref object for the menu anchor + * @param {boolean} props.open - Open state of the popover + * @param {(open: boolean) => void} props.toggleOpen - Callback function for open state change + * @returns {React.ReactNode} + */ +function OpacityControl({ menuRef, open, toggleOpen, opacity, onChange }) { + function handleTextFieldChange(e) { + const newOpacity = parseInt(e.target.value || '0'); + const clamped = clamp(newOpacity, 0, 100); + onChange(clamped / 100); + } + + return ( + <> + <Tooltip title='Layer opacity'> + <IconButton + size='small' + aria-label='Toggle opacity control' + color={open ? 'primary' : 'default'} + onClick={() => toggleOpen(!open)} + > + <svg + width='24' + height='24' + viewBox='0 0 24 24' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <path + fillRule='evenodd' + clipRule='evenodd' + d='M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM17.625 19V17.625H19V15.375H17.625V13.125H19V10.875H17.625V8.625H19V6.375H17.625V5H15.375V6.375H13.125V5H10.875V6.375H8.625V5H6.375V6.375H5V8.625H6.375V10.875H5V13.125H6.375V15.375H5V17.625H6.375V19H8.625V17.625H10.875V19H13.125V17.625H15.375V19H17.625ZM15.375 15.375H17.625V17.625H15.375V15.375ZM13.125 15.375H15.375V13.125H17.625V10.875H15.375V8.625H17.625V6.375H15.375V8.625H13.125V6.375H10.875V8.625H8.625V6.375H6.375V8.625H8.625V10.875H6.375V13.125H8.625V15.375H6.375V17.625H8.625V15.375H10.875V17.625H13.125V15.375ZM13.125 13.125H15.375V10.875H13.125V8.625H10.875V10.875H8.625V13.125H10.875V15.375H13.125V13.125ZM13.125 13.125H10.875V10.875H13.125V13.125Z' + /> + </svg> + </IconButton> + </Tooltip> + <Popover + open={open} + onClose={() => toggleOpen(false)} + anchorEl={menuRef.current} + anchorOrigin={{ + vertical: 'top', + horizontal: 'right' + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'right' + }} + slotProps={{ + root: { + sx: { transform: 'translate(-12px, 36px)' } + } + }} + > + <Box sx={styles.opacityControl}> + <Slider + value={opacity * 100} + onChange={(_, v) => onChange(v / 100)} + min={0} + max={100} + step={1} + /> + <TextField + value={Math.round(opacity * 100)} + size='small' + onChange={handleTextFieldChange} + type='number' + sx={styles.opacityInput} + inputProps={{ + step: 1, + min: 0, + max: 100, + style: { appearance: 'textfield' } + }} + InputProps={{ + endAdornment: ( + <InputAdornment position='end' sx={{ margin: 0 }}> + {' '} + % + </InputAdornment> + ) + }} + /> + </Box> + </Popover> + </> + ); +} + +/** + * @param {object} props + * @param {number} props.minZoom - Global minimum zoom level for the map. + * @param {number} props.maxZoom - Global maximum zoom level for the map. + * @param {number} props.layerMinZoom - Layer minimum zoom level. + * @param {number} props.layerMaxZoom - Layer maximum zoom level. + * @returns {string} + */ +function getZoomHelperText({ minZoom, maxZoom, layerMinZoom, layerMaxZoom }) { + const maxZoomText = layerMaxZoom < maxZoom ? `lower than ${layerMaxZoom}` : ''; + const minZoomText = layerMinZoom > minZoom ? `greater than ${layerMinZoom}` : ''; + const texts = [maxZoomText, minZoomText].filter(Boolean).join(' and '); + return texts ? `Note: this layer will display at zoom levels ${texts}` : ''; +} + +/** + * @param {object} props + * @param {string} props.title + * @param {boolean} props.visible + * @returns {React.ReactNode} + */ +function LegendItemTitle({ title, visible }) { + const ref = useRef(null); + const [isOverflow, setIsOverflow] = useState(false); + + useLayoutEffect(() => { + if (visible && ref.current) { + const { offsetWidth, scrollWidth } = ref.current; + setIsOverflow(offsetWidth < scrollWidth); + } + }, [title, visible]); + + const element = ( + <Typography + ref={ref} + color={visible ? 'textPrimary' : 'textSecondary'} + variant='button' + fontWeight={500} + lineHeight='20px' + component='p' + noWrap + sx={{ my: 0.25 }} + > + {title} + </Typography> + ); + + if (!isOverflow) { + return element; + } + + return <Tooltip title={title}>{element}</Tooltip>; +} diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js index 1e41eb01f..fbf75bbb5 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js @@ -1,135 +1,13 @@ import PropTypes from 'prop-types'; -import { - Box, - Collapse, - IconButton, - InputAdornment, - Paper, - Popover, - Slider, - TextField, - Tooltip, - Typography -} from '@mui/material'; -import { LEGEND_TYPES, LegendCategories, LegendIcon, LegendRamp } from '@carto/react-ui'; -import EyeIcon from '@mui/icons-material/VisibilityOutlined'; -import EyeOffIcon from '@mui/icons-material/VisibilityOffOutlined'; +import { Box, Collapse, IconButton, Paper, Tooltip, Typography } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import LayerIcon from '@mui/icons-material/LayersOutlined'; -import { useLayoutEffect, useRef, useState } from 'react'; +import { LEGEND_WIDTH, styles } from './LegendWidgetUI.styles'; +import LegendItem from './LegendItem'; const EMPTY_OBJ = {}; const EMPTY_FN = () => {}; const EMPTY_ARR = []; -const LEGEND_WIDTH = 240; - -/** - * Returns a number whose value is limited to the given range. - * - * @param {Number} val The initial value - * @param {Number} min The lower boundary - * @param {Number} max The upper boundary - * @returns {Number} A number in the range (min, max) - */ -const clamp = (val, min, max) => Math.min(Math.max(val, min), max); - -export const styles = { - root: { - background: (theme) => theme.palette.background.paper, - position: 'absolute', - maxHeight: 'calc(100% - 120px)', - display: 'flex', - flexDirection: 'column' - }, - legendToggleOpen: { - borderBottom: (theme) => `1px solid ${theme.palette.divider}` - }, - legendToggle: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - pl: 2, - pr: 1, - py: 1 - }, - legendItemList: { - overflow: 'auto', - maxHeight: `calc(100% - 12px)` - }, - legendItem: { - '&:not(:first-of-type)': { - borderTop: (theme) => `1px solid ${theme.palette.divider}` - } - }, - legendItemHeader: { - p: 1.5, - pr: 2, - gap: 0.5, - display: 'flex', - justifyContent: 'space-between', - position: 'sticky', - top: 0, - background: (theme) => theme.palette.background.paper - }, - legendItemBody: { - px: 2, - '& [data-testid="categories-legend"] > .MuiGrid-root': { - paddingTop: '6px', - paddingBottom: '6px' - }, - '& [data-testid="icon-legend"] > .MuiGrid-root': { - paddingTop: '2px', - paddingBottom: '2px', - '& > .MuiBox-root': { - width: '20px', - height: '20px', - marginRight: '8px' - }, - '& img': { - display: 'block', - margin: 'auto', - width: 'auto', - height: '20px' - } - } - }, - opacityControl: { - display: 'flex', - gap: 2, - alignItems: 'center', - p: 1, - minWidth: LEGEND_WIDTH - 32 - }, - layerOptions: { - background: (theme) => theme.palette.background.default, - px: 2, - py: 1, - m: 2 - }, - opacityInput: { - display: 'flex', - width: '60px', - flexShrink: 0 - }, - 'top-left': { - top: 0, - left: 0 - }, - 'top-right': { - top: 0, - right: 0 - }, - 'bottom-left': { - bottom: 0, - left: 0 - }, - 'bottom-right': { - bottom: 0, - right: 0 - } -}; /** * @param {object} props @@ -236,288 +114,3 @@ NewLegendWidgetUI.propTypes = { }; export default NewLegendWidgetUI; - -/** - * Receives configuration options, send change events and renders a legend item - * @param {object} props - * @param {import('../legend/LegendWidgetUI').LegendData} props.layer - Layer object from redux store. - * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} props.onChangeCollapsed - Callback function for layer visibility change. - * @param {({ id, opacity }: { id: string, opacity: number }) => void} props.onChangeOpacity - Callback function for layer opacity change. - * @param {({ id, visible }: { id: string, visible: boolean }) => void} props.onChangeVisibility - Callback function for layer collapsed state change. - * @param {number} [props.maxZoom] - Global maximum zoom level for the map. - * @param {number} [props.minZoom] - Global minimum zoom level for the map. - * @param {number} [props.currentZoom] - Current zoom level for the map. - * @returns {React.ReactNode} - */ -export function LegendItem({ - layer = EMPTY_OBJ, - onChangeCollapsed, - onChangeOpacity, - onChangeVisibility, - maxZoom, - minZoom, - currentZoom -}) { - // layer legend defaults as defined here: https://docs.carto.com/carto-for-developers/carto-for-react/library-reference/widgets#legendwidget - const id = layer?.id; - const type = layer?.legend?.type; - const visible = layer?.visible ?? true; - const switchable = layer.switchable ?? true; - const collapsed = layer.collapsed ?? false; - const collapsible = layer.collapsible ?? true; - const opacity = layer.opacity ?? 1; - const showOpacityControl = layer.showOpacityControl ?? true; - - const isExpanded = visible && !collapsed; - const collapseIcon = isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />; - - const [opacityOpen, setOpacityOpen] = useState(false); - const menuAnchorRef = useRef(null); - const layerHasZoom = layer?.minZoom !== undefined || layer?.maxZoom !== undefined; - const showZoomNote = - layerHasZoom && (layer.minZoom > minZoom || layer.maxZoom < maxZoom); - const outsideCurrentZoom = currentZoom < layer.minZoom || currentZoom > layer.maxZoom; - - const zoomHelperText = getZoomHelperText({ - minZoom, - maxZoom, - layerMinZoom: layer.minZoom, - layerMaxZoom: layer.maxZoom - }); - const helperText = layer.helperText ?? showZoomNote ? zoomHelperText : ''; - - if (!layer) { - return null; - } - - return ( - <Box - component='section' - sx={{ - '&:not(:first-of-type)': { - borderTop: (theme) => `1px solid ${theme.palette.divider}` - } - }} - > - <Box ref={menuAnchorRef} component='header' sx={styles.legendItemHeader}> - {collapsible && ( - <IconButton - size='small' - aria-label='Toggle legend item collapsed' - disabled={!visible} - onClick={() => onChangeCollapsed({ id, collapsed: !collapsed })} - > - {collapseIcon} - </IconButton> - )} - <Box flexGrow={1} sx={{ minWidth: 0, flexShrink: 1 }}> - <LegendItemTitle visible={visible} title={layer.title} /> - {showZoomNote && ( - <Typography - color={visible ? 'textPrimary' : 'textSecondary'} - variant='caption' - component='p' - > - Zoom level: {layer.minZoom} - {layer.maxZoom} - </Typography> - )} - </Box> - {showOpacityControl && ( - <OpacityControl - menuRef={menuAnchorRef} - open={opacityOpen} - toggleOpen={setOpacityOpen} - opacity={opacity} - onChange={(opacity) => onChangeOpacity({ id, opacity })} - /> - )} - {switchable && ( - <Tooltip title={visible ? 'Hide layer' : 'Show layer'}> - <IconButton - size='small' - onClick={() => - onChangeVisibility({ - id, - collapsed: visible ? collapsed : false, - visible: !visible - }) - } - > - {visible ? <EyeIcon /> : <EyeOffIcon />} - </IconButton> - </Tooltip> - )} - </Box> - <Collapse unmountOnExit timeout={100} sx={styles.legendItemBody} in={isExpanded}> - <Box pb={2} opacity={outsideCurrentZoom ? 0.5 : 1}> - {type === LEGEND_TYPES.CATEGORY && ( - <LegendCategories layer={layer} legend={layer.legend} /> - )} - {type === LEGEND_TYPES.ICON && ( - <LegendIcon layer={layer} legend={layer.legend} /> - )} - {type === LEGEND_TYPES.BINS && ( - <LegendRamp layer={layer} legend={layer.legend} /> - )} - </Box> - {helperText && ( - <Typography - variant='caption' - color='textSecondary' - component='p' - sx={{ py: 2 }} - > - {helperText} - </Typography> - )} - </Collapse> - </Box> - ); -} - -/** - * @param {object} props - * @param {number} props.opacity - Opacity value - * @param {(opacity: number) => void} props.onChange - Callback function for opacity change - * @param {React.MutableRefObject} props.menuRef - Ref object for the menu anchor - * @param {boolean} props.open - Open state of the popover - * @param {(open: boolean) => void} props.toggleOpen - Callback function for open state change - * @returns {React.ReactNode} - */ -function OpacityControl({ menuRef, open, toggleOpen, opacity, onChange }) { - function handleTextFieldChange(e) { - const newOpacity = parseInt(e.target.value || '0'); - const clamped = clamp(newOpacity, 0, 100); - onChange(clamped / 100); - } - - return ( - <> - <Tooltip title='Layer opacity'> - <IconButton - size='small' - aria-label='Toggle opacity control' - color={open ? 'primary' : 'default'} - onClick={() => toggleOpen(!open)} - > - <svg - width='24' - height='24' - viewBox='0 0 24 24' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM17.625 19V17.625H19V15.375H17.625V13.125H19V10.875H17.625V8.625H19V6.375H17.625V5H15.375V6.375H13.125V5H10.875V6.375H8.625V5H6.375V6.375H5V8.625H6.375V10.875H5V13.125H6.375V15.375H5V17.625H6.375V19H8.625V17.625H10.875V19H13.125V17.625H15.375V19H17.625ZM15.375 15.375H17.625V17.625H15.375V15.375ZM13.125 15.375H15.375V13.125H17.625V10.875H15.375V8.625H17.625V6.375H15.375V8.625H13.125V6.375H10.875V8.625H8.625V6.375H6.375V8.625H8.625V10.875H6.375V13.125H8.625V15.375H6.375V17.625H8.625V15.375H10.875V17.625H13.125V15.375ZM13.125 13.125H15.375V10.875H13.125V8.625H10.875V10.875H8.625V13.125H10.875V15.375H13.125V13.125ZM13.125 13.125H10.875V10.875H13.125V13.125Z' - /> - </svg> - </IconButton> - </Tooltip> - <Popover - open={open} - onClose={() => toggleOpen(false)} - anchorEl={menuRef.current} - anchorOrigin={{ - vertical: 'top', - horizontal: 'right' - }} - transformOrigin={{ - vertical: 'top', - horizontal: 'right' - }} - slotProps={{ - root: { - sx: { transform: 'translate(-12px, 36px)' } - } - }} - > - <Box sx={styles.opacityControl}> - <Slider - value={opacity * 100} - onChange={(_, v) => onChange(v / 100)} - min={0} - max={100} - step={1} - /> - <TextField - value={Math.round(opacity * 100)} - size='small' - onChange={handleTextFieldChange} - type='number' - sx={styles.opacityInput} - inputProps={{ - step: 1, - min: 0, - max: 100, - style: { appearance: 'textfield' } - }} - InputProps={{ - endAdornment: ( - <InputAdornment position='end' sx={{ margin: 0 }}> - {' '} - % - </InputAdornment> - ) - }} - /> - </Box> - </Popover> - </> - ); -} - -/** - * @param {object} props - * @param {number} props.minZoom - Global minimum zoom level for the map. - * @param {number} props.maxZoom - Global maximum zoom level for the map. - * @param {number} props.layerMinZoom - Layer minimum zoom level. - * @param {number} props.layerMaxZoom - Layer maximum zoom level. - * @returns {string} - */ -function getZoomHelperText({ minZoom, maxZoom, layerMinZoom, layerMaxZoom }) { - const maxZoomText = layerMaxZoom < maxZoom ? `lower than ${layerMaxZoom}` : ''; - const minZoomText = layerMinZoom > minZoom ? `greater than ${layerMinZoom}` : ''; - const texts = [maxZoomText, minZoomText].filter(Boolean).join(' and '); - return texts ? `Note: this layer will display at zoom levels ${texts}` : ''; -} - -/** - * @param {object} props - * @param {string} props.title - * @param {boolean} props.visible - * @returns {React.ReactNode} - */ -function LegendItemTitle({ title, visible }) { - const ref = useRef(null); - const [isOverflow, setIsOverflow] = useState(false); - - useLayoutEffect(() => { - if (visible && ref.current) { - const { offsetWidth, scrollWidth } = ref.current; - setIsOverflow(offsetWidth < scrollWidth); - } - }, [title, visible]); - - const element = ( - <Typography - ref={ref} - color={visible ? 'textPrimary' : 'textSecondary'} - variant='button' - fontWeight={500} - lineHeight='20px' - component='p' - noWrap - sx={{ my: 0.25 }} - > - {title} - </Typography> - ); - - if (!isOverflow) { - return element; - } - - return <Tooltip title={title}>{element}</Tooltip>; -} diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js new file mode 100644 index 000000000..26a7c3958 --- /dev/null +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js @@ -0,0 +1,96 @@ +export const LEGEND_WIDTH = 240; +export const styles = { + root: { + background: (theme) => theme.palette.background.paper, + position: 'absolute', + maxHeight: 'calc(100% - 120px)', + display: 'flex', + flexDirection: 'column' + }, + legendToggleOpen: { + borderBottom: (theme) => `1px solid ${theme.palette.divider}` + }, + legendToggle: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + pl: 2, + pr: 1, + py: 1 + }, + legendItemList: { + overflow: 'auto', + maxHeight: `calc(100% - 12px)` + }, + legendItem: { + '&:not(:first-of-type)': { + borderTop: (theme) => `1px solid ${theme.palette.divider}` + } + }, + legendItemHeader: { + p: 1.5, + pr: 2, + gap: 0.5, + display: 'flex', + justifyContent: 'space-between', + position: 'sticky', + top: 0, + background: (theme) => theme.palette.background.paper + }, + legendItemBody: { + px: 2 + // '& [data-testid="categories-legend"] > .MuiGrid-root': { + // paddingTop: '6px', + // paddingBottom: '6px' + // }, + // '& [data-testid="icon-legend"] > .MuiGrid-root': { + // paddingTop: '2px', + // paddingBottom: '2px', + // '& > .MuiBox-root': { + // width: '20px', + // height: '20px', + // marginRight: '8px' + // }, + // '& img': { + // display: 'block', + // margin: 'auto', + // width: 'auto', + // height: '20px' + // } + // } + }, + opacityControl: { + display: 'flex', + gap: 2, + alignItems: 'center', + p: 1, + minWidth: LEGEND_WIDTH - 32 + }, + layerOptions: { + background: (theme) => theme.palette.background.default, + px: 2, + py: 1, + m: 2 + }, + opacityInput: { + display: 'flex', + width: '60px', + flexShrink: 0 + }, + 'top-left': { + top: 0, + left: 0 + }, + 'top-right': { + top: 0, + right: 0 + }, + 'bottom-left': { + bottom: 0, + left: 0 + }, + 'bottom-right': { + bottom: 0, + right: 0 + } +}; diff --git a/packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js b/packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js index 7ec6bc72b..e0ef24d0b 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js @@ -91,6 +91,7 @@ const Template = ({ ...args }) => { onChangeLegendRowCollapsed={(layer) => dispatch({ type: 'update', layer })} onChangeOpacity={(layer) => dispatch({ type: 'update', layer })} onChangeVisibility={(layer) => dispatch({ type: 'update', layer })} + currentZoom={13} /> ); }; From 4b787840a3b47632178be61f6aae36e607a3646f Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Mon, 5 Feb 2024 12:38:20 +0100 Subject: [PATCH 09/66] disable opacity button when layer is not visible --- packages/react-ui/src/widgets/new-legend/LegendItem.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/react-ui/src/widgets/new-legend/LegendItem.js b/packages/react-ui/src/widgets/new-legend/LegendItem.js index 12b12b3a2..a9e0d99c7 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendItem.js +++ b/packages/react-ui/src/widgets/new-legend/LegendItem.js @@ -108,6 +108,7 @@ export default function LegendItem({ {showOpacityControl && ( <OpacityControl menuRef={menuAnchorRef} + visible={visible} open={opacityOpen} toggleOpen={setOpacityOpen} opacity={opacity} @@ -186,14 +187,15 @@ function clamp(val, min, max) { /** * @param {object} props - * @param {number} props.opacity - Opacity value - * @param {(opacity: number) => void} props.onChange - Callback function for opacity change * @param {React.MutableRefObject} props.menuRef - Ref object for the menu anchor + * @param {boolean} props.visible - Visibility state of the layer * @param {boolean} props.open - Open state of the popover * @param {(open: boolean) => void} props.toggleOpen - Callback function for open state change + * @param {number} props.opacity - Opacity value + * @param {(opacity: number) => void} props.onChange - Callback function for opacity change * @returns {React.ReactNode} */ -function OpacityControl({ menuRef, open, toggleOpen, opacity, onChange }) { +function OpacityControl({ menuRef, visible, open, toggleOpen, opacity, onChange }) { function handleTextFieldChange(e) { const newOpacity = parseInt(e.target.value || '0'); const clamped = clamp(newOpacity, 0, 100); @@ -205,6 +207,7 @@ function OpacityControl({ menuRef, open, toggleOpen, opacity, onChange }) { <Tooltip title='Layer opacity'> <IconButton size='small' + disabled={!visible} aria-label='Toggle opacity control' color={open ? 'primary' : 'default'} onClick={() => toggleOpen(!open)} From 9bb1d39f0cccc85950a39408523e9b2a99894ca6 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Mon, 5 Feb 2024 13:22:35 +0100 Subject: [PATCH 10/66] first step of multi-variable legend --- .../src/widgets/legend/LegendWidgetUI.d.ts | 6 +- .../src/widgets/new-legend/LegendItem.js | 58 +++++++++++++++---- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts index 683d57cda..01dce9711 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts @@ -34,11 +34,11 @@ export type LegendItemData = { select: LegendItemSelectConfig }; -export type LegendItemSelectConfig = { +export type LegendItemSelectConfig<T = unknown> = { label: string; - value: string; + value: T; options: { label: string; - value: string; + value: T; }[]; }; diff --git a/packages/react-ui/src/widgets/new-legend/LegendItem.js b/packages/react-ui/src/widgets/new-legend/LegendItem.js index a9e0d99c7..3f056f249 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendItem.js +++ b/packages/react-ui/src/widgets/new-legend/LegendItem.js @@ -15,7 +15,7 @@ import EyeIcon from '@mui/icons-material/VisibilityOutlined'; import EyeOffIcon from '@mui/icons-material/VisibilityOffOutlined'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; -import { useLayoutEffect, useRef, useState } from 'react'; +import { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { styles } from './LegendWidgetUI.styles'; const EMPTY_OBJ = {}; @@ -27,6 +27,7 @@ const EMPTY_OBJ = {}; * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} props.onChangeCollapsed - Callback function for layer visibility change. * @param {({ id, opacity }: { id: string, opacity: number }) => void} props.onChangeOpacity - Callback function for layer opacity change. * @param {({ id, visible }: { id: string, visible: boolean }) => void} props.onChangeVisibility - Callback function for layer collapsed state change. + * @param {({ id, selection }: { id: string, selection: unknown }) => void} props.onChangeSelection - Callback function for layer selection change. * @param {number} props.maxZoom - Global maximum zoom level for the map. * @param {number} props.minZoom - Global minimum zoom level for the map. * @param {number} props.currentZoom - Current zoom level for the map. @@ -37,6 +38,7 @@ export default function LegendItem({ onChangeCollapsed, onChangeOpacity, onChangeVisibility, + onChangeSelection, maxZoom, minZoom, currentZoom @@ -51,6 +53,13 @@ export default function LegendItem({ const opacity = layer.opacity ?? 1; const showOpacityControl = layer.showOpacityControl ?? true; + const legendItemVariables = useMemo(() => { + if (!layer.legend) { + return []; + } + return Array.isArray(layer.legend) ? layer.legend : [layer.legend]; + }, [layer.legend]); + const isExpanded = visible && !collapsed; const collapseIcon = isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />; @@ -134,15 +143,14 @@ export default function LegendItem({ </Box> <Collapse unmountOnExit timeout={100} sx={styles.legendItemBody} in={isExpanded}> <Box pb={2} opacity={outsideCurrentZoom ? 0.5 : 1}> - {type === LEGEND_TYPES.CATEGORY && ( - <LegendCategories layer={layer} legend={layer.legend} /> - )} - {type === LEGEND_TYPES.ICON && ( - <LegendIcon layer={layer} legend={layer.legend} /> - )} - {type === LEGEND_TYPES.BINS && ( - <LegendRamp layer={layer} legend={layer.legend} /> - )} + {legendItemVariables.map((legend) => ( + <LegendItemVariable + key={legend.type} + legend={legend} + layer={layer} + onChangeSelection={(selection) => onChangeSelection({ id, selection })} + /> + ))} </Box> {helperText && ( <Typography @@ -164,6 +172,7 @@ LegendItem.propTypes = { onChangeCollapsed: PropTypes.func.isRequired, onChangeOpacity: PropTypes.func.isRequired, onChangeVisibility: PropTypes.func.isRequired, + onChangeSelection: PropTypes.func.isRequired, maxZoom: PropTypes.number, minZoom: PropTypes.number, currentZoom: PropTypes.number @@ -333,3 +342,32 @@ function LegendItemTitle({ title, visible }) { return <Tooltip title={title}>{element}</Tooltip>; } + +/** + * @param {object} props + * @param {import('../legend/LegendWidgetUI').LegendData} props.layer - Layer object from redux store. + * @param {import('../legend/LegendWidgetUI').LegendItemData} props.legend - legend variable data. + * @param {({ id, selection }: { id: string, selection: unknown }) => void} props.onChangeSelection - Callback function for legend options change. + * @returns {React.ReactNode} + */ +function LegendItemVariable({ layer, legend, onChangeSelection }) { + const type = legend.type; + + let typeComponent = null; + if (type === LEGEND_TYPES.CATEGORY) { + typeComponent = <LegendCategories layer={layer} legend={layer.legend} />; + } + if (type === LEGEND_TYPES.ICON) { + typeComponent = <LegendIcon layer={layer} legend={layer.legend} />; + } + if (type === LEGEND_TYPES.BINS) { + typeComponent = <LegendRamp layer={layer} legend={layer.legend} />; + } + + return ( + <> + <div id='legend-option-selector'></div> + {typeComponent} + </> + ); +} From c89a5b937880b1a35faadbf10cfbe91cb948d417 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Mon, 5 Feb 2024 13:24:38 +0100 Subject: [PATCH 11/66] show opacity control only when visible --- packages/react-ui/src/widgets/new-legend/LegendItem.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/react-ui/src/widgets/new-legend/LegendItem.js b/packages/react-ui/src/widgets/new-legend/LegendItem.js index 3f056f249..17c98bd9a 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendItem.js +++ b/packages/react-ui/src/widgets/new-legend/LegendItem.js @@ -45,7 +45,6 @@ export default function LegendItem({ }) { // layer legend defaults as defined here: https://docs.carto.com/carto-for-developers/carto-for-react/library-reference/widgets#legendwidget const id = layer?.id; - const type = layer?.legend?.type; const visible = layer?.visible ?? true; const switchable = layer.switchable ?? true; const collapsed = layer.collapsed ?? false; @@ -114,10 +113,9 @@ export default function LegendItem({ </Typography> )} </Box> - {showOpacityControl && ( + {showOpacityControl && visible && ( <OpacityControl menuRef={menuAnchorRef} - visible={visible} open={opacityOpen} toggleOpen={setOpacityOpen} opacity={opacity} @@ -197,14 +195,13 @@ function clamp(val, min, max) { /** * @param {object} props * @param {React.MutableRefObject} props.menuRef - Ref object for the menu anchor - * @param {boolean} props.visible - Visibility state of the layer * @param {boolean} props.open - Open state of the popover * @param {(open: boolean) => void} props.toggleOpen - Callback function for open state change * @param {number} props.opacity - Opacity value * @param {(opacity: number) => void} props.onChange - Callback function for opacity change * @returns {React.ReactNode} */ -function OpacityControl({ menuRef, visible, open, toggleOpen, opacity, onChange }) { +function OpacityControl({ menuRef, open, toggleOpen, opacity, onChange }) { function handleTextFieldChange(e) { const newOpacity = parseInt(e.target.value || '0'); const clamped = clamp(newOpacity, 0, 100); @@ -216,7 +213,6 @@ function OpacityControl({ menuRef, visible, open, toggleOpen, opacity, onChange <Tooltip title='Layer opacity'> <IconButton size='small' - disabled={!visible} aria-label='Toggle opacity control' color={open ? 'primary' : 'default'} onClick={() => toggleOpen(!open)} From 94cd51b4a975a68c2bb800e2dea81597f1a55cc1 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Tue, 6 Feb 2024 16:59:42 +0100 Subject: [PATCH 12/66] add custom legend types config and improve multi legend support --- .../src/widgets/legend/LegendWidgetUI.d.ts | 39 +++++++++++--- .../src/widgets/new-legend/LegendItem.js | 54 +++++++++++++------ .../src/widgets/new-legend/LegendWidgetUI.js | 3 +- .../stories/widgetsUI/legendFixtures.js | 33 +++++++----- 4 files changed, 93 insertions(+), 36 deletions(-) diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts index 01dce9711..cd94099b2 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts @@ -6,7 +6,6 @@ export enum LEGEND_TYPES { CONTINUOUS_RAMP = 'continuous_ramp', BINS = 'bins', PROPORTION = 'proportion', - CUSTOM = 'custom' } export type LegendData = { @@ -26,13 +25,34 @@ export type LegendData = { export type LegendItemData = { type: LEGEND_TYPES; - children?: React.ReactNode; - attr?: React.ReactNode; // subtitle - colors?: string[]; - labels?: (string | number)[]; - icons?: string[]; select: LegendItemSelectConfig -}; + attr?: React.ReactNode; // subtitle to show below the legend item toggle when expanded +} & LegendType; + +type LegendType = LegendBins | LegendRamp | LegendIcons | LegendCategories | LegendProportion; + +type LegendColors = string | string[]; +type LegendNumericLabels = number[] | { label: string; value: number }[]; + +type LegendBins = { + colors: LegendColors + labels: LegendNumericLabels +} +type LegendRamp = { + colors: LegendColors + labels: LegendNumericLabels +} +type LegendIcons = { + icons: string[] + labels: string[] +} +type LegendCategories = { + colors: LegendColors + labels: string[] +} +type LegendProportion = { + labels: [number, number] +} export type LegendItemSelectConfig<T = unknown> = { label: string; @@ -42,3 +62,8 @@ export type LegendItemSelectConfig<T = unknown> = { value: T; }[]; }; + +export type CustomLegendComponent = React.ComponentType<{ + layer: LegendData; + legend: LegendItemData; +}>; diff --git a/packages/react-ui/src/widgets/new-legend/LegendItem.js b/packages/react-ui/src/widgets/new-legend/LegendItem.js index 17c98bd9a..973c9b1fc 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendItem.js +++ b/packages/react-ui/src/widgets/new-legend/LegendItem.js @@ -10,7 +10,13 @@ import { Tooltip, Typography } from '@mui/material'; -import { LEGEND_TYPES, LegendCategories, LegendIcon, LegendRamp } from '@carto/react-ui'; +import { + LEGEND_TYPES, + LegendCategories, + LegendIcon, + LegendProportion, + LegendRamp +} from '@carto/react-ui'; import EyeIcon from '@mui/icons-material/VisibilityOutlined'; import EyeOffIcon from '@mui/icons-material/VisibilityOffOutlined'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; @@ -23,6 +29,7 @@ const EMPTY_OBJ = {}; /** * Receives configuration options, send change events and renders a legend item * @param {object} props + * @param {Object.<string, import('../legend/LegendWidgetUI').CustomLegendComponent>} props.customLegendTypes - Allow to customise by default legend types that can be rendered. * @param {import('../legend/LegendWidgetUI').LegendData} props.layer - Layer object from redux store. * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} props.onChangeCollapsed - Callback function for layer visibility change. * @param {({ id, opacity }: { id: string, opacity: number }) => void} props.onChangeOpacity - Callback function for layer opacity change. @@ -34,6 +41,7 @@ const EMPTY_OBJ = {}; * @returns {React.ReactNode} */ export default function LegendItem({ + customLegendTypes = EMPTY_OBJ, layer = EMPTY_OBJ, onChangeCollapsed, onChangeOpacity, @@ -45,6 +53,7 @@ export default function LegendItem({ }) { // layer legend defaults as defined here: https://docs.carto.com/carto-for-developers/carto-for-react/library-reference/widgets#legendwidget const id = layer?.id; + const title = layer?.title; const visible = layer?.visible ?? true; const switchable = layer.switchable ?? true; const collapsed = layer.collapsed ?? false; @@ -83,6 +92,7 @@ export default function LegendItem({ return ( <Box + aria-label={title} component='section' sx={{ '&:not(:first-of-type)': { @@ -102,7 +112,7 @@ export default function LegendItem({ </IconButton> )} <Box flexGrow={1} sx={{ minWidth: 0, flexShrink: 1 }}> - <LegendItemTitle visible={visible} title={layer.title} /> + <LegendItemTitle visible={visible} title={title} /> {showZoomNote && ( <Typography color={visible ? 'textPrimary' : 'textSecondary'} @@ -146,6 +156,7 @@ export default function LegendItem({ key={legend.type} legend={legend} layer={layer} + customLegendTypes={customLegendTypes} onChangeSelection={(selection) => onChangeSelection({ id, selection })} /> ))} @@ -166,6 +177,7 @@ export default function LegendItem({ } LegendItem.propTypes = { + customLegendTypes: PropTypes.object.isRequired, layer: PropTypes.object.isRequired, onChangeCollapsed: PropTypes.func.isRequired, onChangeOpacity: PropTypes.func.isRequired, @@ -339,31 +351,43 @@ function LegendItemTitle({ title, visible }) { return <Tooltip title={title}>{element}</Tooltip>; } +const legendTypeMap = { + [LEGEND_TYPES.CATEGORY]: LegendCategories, + [LEGEND_TYPES.ICON]: LegendIcon, + [LEGEND_TYPES.BINS]: LegendRamp, + [LEGEND_TYPES.PROPORTION]: LegendProportion, + [LEGEND_TYPES.CONTINUOUS_RAMP]: LegendRamp +}; + +/** + * @param {object} props + * @param {import('../legend/LegendWidgetUI').LegendItemData} props.legend - legend variable data. + * @returns {React.ReactNode} + */ +function LegendUnknown({ legend }) { + return ( + <Typography variant='body2' color='textSecondary' component='p'> + Legend type {legend.type} not supported + </Typography> + ); +} + /** * @param {object} props * @param {import('../legend/LegendWidgetUI').LegendData} props.layer - Layer object from redux store. * @param {import('../legend/LegendWidgetUI').LegendItemData} props.legend - legend variable data. + * @param {Object.<string, import('../legend/LegendWidgetUI').CustomLegendComponent>} props.customLegendTypes - Map from legend type to legend component that allows to customise additional legend types that can be rendered. * @param {({ id, selection }: { id: string, selection: unknown }) => void} props.onChangeSelection - Callback function for legend options change. * @returns {React.ReactNode} */ -function LegendItemVariable({ layer, legend, onChangeSelection }) { +function LegendItemVariable({ layer, legend, customLegendTypes, onChangeSelection }) { const type = legend.type; - - let typeComponent = null; - if (type === LEGEND_TYPES.CATEGORY) { - typeComponent = <LegendCategories layer={layer} legend={layer.legend} />; - } - if (type === LEGEND_TYPES.ICON) { - typeComponent = <LegendIcon layer={layer} legend={layer.legend} />; - } - if (type === LEGEND_TYPES.BINS) { - typeComponent = <LegendRamp layer={layer} legend={layer.legend} />; - } + const TypeComponent = legendTypeMap[type] || customLegendTypes[type] || LegendUnknown; return ( <> <div id='legend-option-selector'></div> - {typeComponent} + <TypeComponent layer={layer} legend={legend} /> </> ); } diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js index fbf75bbb5..ef0f3a20e 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js @@ -11,7 +11,7 @@ const EMPTY_ARR = []; /** * @param {object} props - * @param {Object.<string, Function>} [props.customLegendTypes] - Allow to customise by default legend types that can be rendered. + * @param {Object.<string, import('../legend/LegendWidgetUI').CustomLegendComponent>} [props.customLegendTypes] - Allow to customise by default legend types that can be rendered. * @param {import('../legend/LegendWidgetUI').LegendData[]} [props.layers] - Array of layer objects from redux store. * @param {boolean} [props.collapsed] - Collapsed state for whole legend widget. * @param {(collapsed: boolean) => void} props.onChangeCollapsed - Callback function for collapsed state change. @@ -84,6 +84,7 @@ function NewLegendWidgetUI({ maxZoom={maxZoom} minZoom={minZoom} currentZoom={currentZoom} + customLegendTypes={customLegendTypes} /> ))} </Collapse> diff --git a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js index b7717c579..3f9822443 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js +++ b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js @@ -26,19 +26,26 @@ export const fixtures = [ title: 'Available Land', visible: true, switchable: true, - legend: { - collapsed: false, - type: LEGEND_TYPES.ICON, - icons: [ - `data:image/svg+xml,<svg width="26" height="42" view-box="0 0 26 42" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path fill-rule="evenodd" clip-rule="evenodd" d="M24.9898 13.5C24.9966 13.3342 25 13.1675 25 13C25 6.37258 19.6274 1 13 1C6.37258 1 1 6.37258 1 13C1 13.1675 1.00343 13.3342 1.01023 13.5H1C1 15 1.66667 17.1667 2 18L13 39.5L24 18C24.7689 16.4972 25.0079 14.2072 25 13.5H24.9898Z" fill="#D40511"/> - <path d="M24.9898 13.5L23.9906 13.4591L23.948 14.5H24.9898V13.5ZM1.01023 13.5V14.5H2.05205L2.00939 13.459L1.01023 13.5ZM1 13.5V12.5H0V13.5H1ZM2 18L1.07152 18.3714L1.08869 18.4143L1.10975 18.4555L2 18ZM13 39.5L12.1098 39.9555L13 41.6955L13.8902 39.9555L13 39.5ZM24 18L23.1098 17.5445V17.5445L24 18ZM25 13.5L25.9999 13.4889L25.9889 12.5H25V13.5ZM24 13C24 13.1538 23.9968 13.3069 23.9906 13.4591L25.9889 13.5409C25.9963 13.3615 26 13.1811 26 13H24ZM13 2C19.0751 2 24 6.92487 24 13H26C26 5.8203 20.1797 0 13 0V2ZM2 13C2 6.92487 6.92487 2 13 2V0C5.8203 0 0 5.8203 0 13H2ZM2.00939 13.459C2.00315 13.3069 2 13.1538 2 13H0C0 13.1811 0.00371128 13.3615 0.0110667 13.541L2.00939 13.459ZM1 14.5H1.01023V12.5H1V14.5ZM2.92848 17.6286C2.78084 17.2595 2.54409 16.5532 2.34514 15.7575C2.14373 14.9518 2 14.1282 2 13.5H0C0 14.3718 0.189607 15.3815 0.404858 16.2425C0.622579 17.1134 0.885825 17.9071 1.07152 18.3714L2.92848 17.6286ZM13.8902 39.0445L2.89025 17.5445L1.10975 18.4555L12.1098 39.9555L13.8902 39.0445ZM23.1098 17.5445L12.1098 39.0445L13.8902 39.9555L24.8902 18.4555L23.1098 17.5445ZM24.0001 13.5111C24.0031 13.7838 23.9544 14.4669 23.8075 15.2721C23.6602 16.0796 23.43 16.9186 23.1098 17.5445L24.8902 18.4555C25.3389 17.5786 25.6126 16.5212 25.775 15.6311C25.9379 14.7388 26.0048 13.9234 25.9999 13.4889L24.0001 13.5111ZM24.9898 14.5H25V12.5H24.9898V14.5Z" fill="white"/> - <circle id="Ellipse 10" cx="13" cy="13" r="3" fill="white" stroke="white" stroke-width="2"/> - </svg>` - ].map((txt) => txt.replace(/#/g, '%23')), - colors: ['#D40511'], - labels: ['Available land'] - } + legend: [ + { + collapsed: false, + type: LEGEND_TYPES.ICON, + icons: [ + `data:image/svg+xml,<svg width="26" height="42" view-box="0 0 26 42" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M24.9898 13.5C24.9966 13.3342 25 13.1675 25 13C25 6.37258 19.6274 1 13 1C6.37258 1 1 6.37258 1 13C1 13.1675 1.00343 13.3342 1.01023 13.5H1C1 15 1.66667 17.1667 2 18L13 39.5L24 18C24.7689 16.4972 25.0079 14.2072 25 13.5H24.9898Z" fill="#D40511"/> + <path d="M24.9898 13.5L23.9906 13.4591L23.948 14.5H24.9898V13.5ZM1.01023 13.5V14.5H2.05205L2.00939 13.459L1.01023 13.5ZM1 13.5V12.5H0V13.5H1ZM2 18L1.07152 18.3714L1.08869 18.4143L1.10975 18.4555L2 18ZM13 39.5L12.1098 39.9555L13 41.6955L13.8902 39.9555L13 39.5ZM24 18L23.1098 17.5445V17.5445L24 18ZM25 13.5L25.9999 13.4889L25.9889 12.5H25V13.5ZM24 13C24 13.1538 23.9968 13.3069 23.9906 13.4591L25.9889 13.5409C25.9963 13.3615 26 13.1811 26 13H24ZM13 2C19.0751 2 24 6.92487 24 13H26C26 5.8203 20.1797 0 13 0V2ZM2 13C2 6.92487 6.92487 2 13 2V0C5.8203 0 0 5.8203 0 13H2ZM2.00939 13.459C2.00315 13.3069 2 13.1538 2 13H0C0 13.1811 0.00371128 13.3615 0.0110667 13.541L2.00939 13.459ZM1 14.5H1.01023V12.5H1V14.5ZM2.92848 17.6286C2.78084 17.2595 2.54409 16.5532 2.34514 15.7575C2.14373 14.9518 2 14.1282 2 13.5H0C0 14.3718 0.189607 15.3815 0.404858 16.2425C0.622579 17.1134 0.885825 17.9071 1.07152 18.3714L2.92848 17.6286ZM13.8902 39.0445L2.89025 17.5445L1.10975 18.4555L12.1098 39.9555L13.8902 39.0445ZM23.1098 17.5445L12.1098 39.0445L13.8902 39.9555L24.8902 18.4555L23.1098 17.5445ZM24.0001 13.5111C24.0031 13.7838 23.9544 14.4669 23.8075 15.2721C23.6602 16.0796 23.43 16.9186 23.1098 17.5445L24.8902 18.4555C25.3389 17.5786 25.6126 16.5212 25.775 15.6311C25.9379 14.7388 26.0048 13.9234 25.9999 13.4889L24.0001 13.5111ZM24.9898 14.5H25V12.5H24.9898V14.5Z" fill="white"/> + <circle id="Ellipse 10" cx="13" cy="13" r="3" fill="white" stroke="white" stroke-width="2"/> + </svg>` + ].map((txt) => txt.replace(/#/g, '%23')), + colors: ['#D40511'], + labels: ['Available land'] + }, + { + collapsed: false, + type: LEGEND_TYPES.PROPORTION, + labels: [1, 1000] + } + ] }, { id: 'catchment', From f1d26f176fc05559fc560817821bc0256e2dc826 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Tue, 6 Feb 2024 17:28:49 +0100 Subject: [PATCH 13/66] extract opacity component --- .../src/widgets/new-legend/LegendItem.js | 119 +----------------- .../new-legend/LegendOpacityControl.js | 114 +++++++++++++++++ 2 files changed, 117 insertions(+), 116 deletions(-) create mode 100644 packages/react-ui/src/widgets/new-legend/LegendOpacityControl.js diff --git a/packages/react-ui/src/widgets/new-legend/LegendItem.js b/packages/react-ui/src/widgets/new-legend/LegendItem.js index 973c9b1fc..04d982661 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendItem.js +++ b/packages/react-ui/src/widgets/new-legend/LegendItem.js @@ -1,15 +1,5 @@ import PropTypes from 'prop-types'; -import { - Box, - Collapse, - IconButton, - InputAdornment, - Popover, - Slider, - TextField, - Tooltip, - Typography -} from '@mui/material'; +import { Box, Collapse, IconButton, Tooltip, Typography } from '@mui/material'; import { LEGEND_TYPES, LegendCategories, @@ -23,6 +13,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { styles } from './LegendWidgetUI.styles'; +import LegendOpacityControl from './LegendOpacityControl'; const EMPTY_OBJ = {}; @@ -124,7 +115,7 @@ export default function LegendItem({ )} </Box> {showOpacityControl && visible && ( - <OpacityControl + <LegendOpacityControl menuRef={menuAnchorRef} open={opacityOpen} toggleOpen={setOpacityOpen} @@ -193,110 +184,6 @@ LegendItem.defaultProps = { currentZoom: 0 }; -/** - * Returns a number whose value is limited to the given range. - * @param {Number} val The initial value - * @param {Number} min The lower boundary - * @param {Number} max The upper boundary - * @returns {Number} A number in the range (min, max) - */ -function clamp(val, min, max) { - return Math.min(Math.max(val, min), max); -} - -/** - * @param {object} props - * @param {React.MutableRefObject} props.menuRef - Ref object for the menu anchor - * @param {boolean} props.open - Open state of the popover - * @param {(open: boolean) => void} props.toggleOpen - Callback function for open state change - * @param {number} props.opacity - Opacity value - * @param {(opacity: number) => void} props.onChange - Callback function for opacity change - * @returns {React.ReactNode} - */ -function OpacityControl({ menuRef, open, toggleOpen, opacity, onChange }) { - function handleTextFieldChange(e) { - const newOpacity = parseInt(e.target.value || '0'); - const clamped = clamp(newOpacity, 0, 100); - onChange(clamped / 100); - } - - return ( - <> - <Tooltip title='Layer opacity'> - <IconButton - size='small' - aria-label='Toggle opacity control' - color={open ? 'primary' : 'default'} - onClick={() => toggleOpen(!open)} - > - <svg - width='24' - height='24' - viewBox='0 0 24 24' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM17.625 19V17.625H19V15.375H17.625V13.125H19V10.875H17.625V8.625H19V6.375H17.625V5H15.375V6.375H13.125V5H10.875V6.375H8.625V5H6.375V6.375H5V8.625H6.375V10.875H5V13.125H6.375V15.375H5V17.625H6.375V19H8.625V17.625H10.875V19H13.125V17.625H15.375V19H17.625ZM15.375 15.375H17.625V17.625H15.375V15.375ZM13.125 15.375H15.375V13.125H17.625V10.875H15.375V8.625H17.625V6.375H15.375V8.625H13.125V6.375H10.875V8.625H8.625V6.375H6.375V8.625H8.625V10.875H6.375V13.125H8.625V15.375H6.375V17.625H8.625V15.375H10.875V17.625H13.125V15.375ZM13.125 13.125H15.375V10.875H13.125V8.625H10.875V10.875H8.625V13.125H10.875V15.375H13.125V13.125ZM13.125 13.125H10.875V10.875H13.125V13.125Z' - /> - </svg> - </IconButton> - </Tooltip> - <Popover - open={open} - onClose={() => toggleOpen(false)} - anchorEl={menuRef.current} - anchorOrigin={{ - vertical: 'top', - horizontal: 'right' - }} - transformOrigin={{ - vertical: 'top', - horizontal: 'right' - }} - slotProps={{ - root: { - sx: { transform: 'translate(-12px, 36px)' } - } - }} - > - <Box sx={styles.opacityControl}> - <Slider - value={opacity * 100} - onChange={(_, v) => onChange(v / 100)} - min={0} - max={100} - step={1} - /> - <TextField - value={Math.round(opacity * 100)} - size='small' - onChange={handleTextFieldChange} - type='number' - sx={styles.opacityInput} - inputProps={{ - step: 1, - min: 0, - max: 100, - style: { appearance: 'textfield' } - }} - InputProps={{ - endAdornment: ( - <InputAdornment position='end' sx={{ margin: 0 }}> - {' '} - % - </InputAdornment> - ) - }} - /> - </Box> - </Popover> - </> - ); -} - /** * @param {object} props * @param {number} props.minZoom - Global minimum zoom level for the map. diff --git a/packages/react-ui/src/widgets/new-legend/LegendOpacityControl.js b/packages/react-ui/src/widgets/new-legend/LegendOpacityControl.js new file mode 100644 index 000000000..d1b1503e9 --- /dev/null +++ b/packages/react-ui/src/widgets/new-legend/LegendOpacityControl.js @@ -0,0 +1,114 @@ +import { + Box, + IconButton, + InputAdornment, + Popover, + Slider, + TextField, + Tooltip +} from '@mui/material'; +import { styles } from './LegendWidgetUI.styles'; +import { useIntl } from 'react-intl'; +import useImperativeIntl from '../../hooks/useImperativeIntl'; + +/** + * @param {object} props + * @param {React.MutableRefObject} props.menuRef - Ref object for the menu anchor + * @param {boolean} props.open - Open state of the popover + * @param {(open: boolean) => void} props.toggleOpen - Callback function for open state change + * @param {number} props.opacity - Opacity value + * @param {(opacity: number) => void} props.onChange - Callback function for opacity change + * @returns {React.ReactNode} + */ +export default function LegendOpacityControl({ + menuRef, + open, + toggleOpen, + opacity, + onChange +}) { + const intl = useIntl(); + const intlConfig = useImperativeIntl(intl); + + function handleTextFieldChange(e) { + const newOpacity = parseInt(e.target.value || '0'); + const clamped = Math.min(Math.max(newOpacity, 0), 100); + onChange(clamped / 100); + } + + return ( + <> + <Tooltip title={intlConfig.formatMessage({ id: 'c4r.widgets.legend.opacity' })}> + <IconButton + size='small' + color={open ? 'primary' : 'default'} + onClick={() => toggleOpen(!open)} + > + <svg + width='24' + height='24' + viewBox='0 0 24 24' + fill='none' + xmlns='http://www.w3.org/2000/svg' + > + <path + fillRule='evenodd' + clipRule='evenodd' + d='M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM17.625 19V17.625H19V15.375H17.625V13.125H19V10.875H17.625V8.625H19V6.375H17.625V5H15.375V6.375H13.125V5H10.875V6.375H8.625V5H6.375V6.375H5V8.625H6.375V10.875H5V13.125H6.375V15.375H5V17.625H6.375V19H8.625V17.625H10.875V19H13.125V17.625H15.375V19H17.625ZM15.375 15.375H17.625V17.625H15.375V15.375ZM13.125 15.375H15.375V13.125H17.625V10.875H15.375V8.625H17.625V6.375H15.375V8.625H13.125V6.375H10.875V8.625H8.625V6.375H6.375V8.625H8.625V10.875H6.375V13.125H8.625V15.375H6.375V17.625H8.625V15.375H10.875V17.625H13.125V15.375ZM13.125 13.125H15.375V10.875H13.125V8.625H10.875V10.875H8.625V13.125H10.875V15.375H13.125V13.125ZM13.125 13.125H10.875V10.875H13.125V13.125Z' + /> + </svg> + </IconButton> + </Tooltip> + <Popover + open={open} + onClose={() => toggleOpen(false)} + anchorEl={menuRef.current} + anchorOrigin={{ + vertical: 'top', + horizontal: 'right' + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'right' + }} + slotProps={{ + root: { + sx: { transform: 'translate(-12px, 36px)' } + } + }} + > + <Box sx={styles.opacityControl}> + <Slider + value={opacity * 100} + onChange={(_, v) => onChange(v / 100)} + min={0} + max={100} + step={1} + /> + <TextField + size='small' + type='number' + value={Math.round(opacity * 100)} + onChange={handleTextFieldChange} + sx={styles.opacityInput} + inputProps={{ + step: 1, + min: 0, + max: 100, + style: { appearance: 'textfield' }, + 'data-testid': 'opacity-slider' + }} + InputProps={{ + endAdornment: ( + <InputAdornment position='end' sx={{ margin: 0 }}> + {' '} + % + </InputAdornment> + ) + }} + /> + </Box> + </Popover> + </> + ); +} From f58780cd2ebc8c7dcde78eb379bb129dfffbbe8f Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Tue, 6 Feb 2024 17:32:22 +0100 Subject: [PATCH 14/66] rename types --- .../react-ui/src/widgets/legend/LegendWidgetUI.d.ts | 10 +++++----- packages/react-ui/src/widgets/new-legend/LegendItem.js | 8 ++++---- .../react-ui/src/widgets/new-legend/LegendWidgetUI.js | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts index cd94099b2..9022035f3 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts @@ -8,7 +8,7 @@ export enum LEGEND_TYPES { PROPORTION = 'proportion', } -export type LegendData = { +export type LegendLayer = { id: string; title?: string; visible?: boolean; // layer visibility state @@ -20,10 +20,10 @@ export type LegendData = { helperText?: React.ReactNode; // note to show below all legend items minZoom?: number; // min zoom at which layer is displayed maxZoom?: number; // max zoom at which layer is displayed - legend?: LegendItemData | LegendItemData[]; + legend?: LegendLayerVariable | LegendLayerVariable[]; }; -export type LegendItemData = { +export type LegendLayerVariable = { type: LEGEND_TYPES; select: LegendItemSelectConfig attr?: React.ReactNode; // subtitle to show below the legend item toggle when expanded @@ -64,6 +64,6 @@ export type LegendItemSelectConfig<T = unknown> = { }; export type CustomLegendComponent = React.ComponentType<{ - layer: LegendData; - legend: LegendItemData; + layer: LegendLayer; + legend: LegendLayerVariable; }>; diff --git a/packages/react-ui/src/widgets/new-legend/LegendItem.js b/packages/react-ui/src/widgets/new-legend/LegendItem.js index 04d982661..fa2b85171 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendItem.js +++ b/packages/react-ui/src/widgets/new-legend/LegendItem.js @@ -21,7 +21,7 @@ const EMPTY_OBJ = {}; * Receives configuration options, send change events and renders a legend item * @param {object} props * @param {Object.<string, import('../legend/LegendWidgetUI').CustomLegendComponent>} props.customLegendTypes - Allow to customise by default legend types that can be rendered. - * @param {import('../legend/LegendWidgetUI').LegendData} props.layer - Layer object from redux store. + * @param {import('../legend/LegendWidgetUI').LegendLayer} props.layer - Layer object from redux store. * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} props.onChangeCollapsed - Callback function for layer visibility change. * @param {({ id, opacity }: { id: string, opacity: number }) => void} props.onChangeOpacity - Callback function for layer opacity change. * @param {({ id, visible }: { id: string, visible: boolean }) => void} props.onChangeVisibility - Callback function for layer collapsed state change. @@ -248,7 +248,7 @@ const legendTypeMap = { /** * @param {object} props - * @param {import('../legend/LegendWidgetUI').LegendItemData} props.legend - legend variable data. + * @param {import('../legend/LegendWidgetUI').LegendLayerVariable} props.legend - legend variable data. * @returns {React.ReactNode} */ function LegendUnknown({ legend }) { @@ -261,8 +261,8 @@ function LegendUnknown({ legend }) { /** * @param {object} props - * @param {import('../legend/LegendWidgetUI').LegendData} props.layer - Layer object from redux store. - * @param {import('../legend/LegendWidgetUI').LegendItemData} props.legend - legend variable data. + * @param {import('../legend/LegendWidgetUI').LegendLayer} props.layer - Layer object from redux store. + * @param {import('../legend/LegendWidgetUI').LegendLayerVariable} props.legend - legend variable data. * @param {Object.<string, import('../legend/LegendWidgetUI').CustomLegendComponent>} props.customLegendTypes - Map from legend type to legend component that allows to customise additional legend types that can be rendered. * @param {({ id, selection }: { id: string, selection: unknown }) => void} props.onChangeSelection - Callback function for legend options change. * @returns {React.ReactNode} diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js index ef0f3a20e..3b4a34924 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js @@ -12,7 +12,7 @@ const EMPTY_ARR = []; /** * @param {object} props * @param {Object.<string, import('../legend/LegendWidgetUI').CustomLegendComponent>} [props.customLegendTypes] - Allow to customise by default legend types that can be rendered. - * @param {import('../legend/LegendWidgetUI').LegendData[]} [props.layers] - Array of layer objects from redux store. + * @param {import('../legend/LegendWidgetUI').LegendLayer[]} [props.layers] - Array of layer objects from redux store. * @param {boolean} [props.collapsed] - Collapsed state for whole legend widget. * @param {(collapsed: boolean) => void} props.onChangeCollapsed - Callback function for collapsed state change. * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} props.onChangeLegendRowCollapsed - Callback function for layer visibility change. From aed0d6bdb2f7a775f8c66911b16f1c163ff3dcc7 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Tue, 6 Feb 2024 17:52:26 +0100 Subject: [PATCH 15/66] rename legendItem to legendLayer and split components more --- .../src/widgets/legend/LegendWidgetUI.d.ts | 14 +- .../{LegendItem.js => LegendLayer.js} | 129 +++--------------- .../widgets/new-legend/LegendLayerTitle.js | 41 ++++++ .../widgets/new-legend/LegendLayerVariable.js | 52 +++++++ .../src/widgets/new-legend/LegendWidgetUI.js | 6 +- 5 files changed, 123 insertions(+), 119 deletions(-) rename packages/react-ui/src/widgets/new-legend/{LegendItem.js => LegendLayer.js} (68%) create mode 100644 packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js create mode 100644 packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts index 9022035f3..b084f6d25 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts @@ -8,7 +8,7 @@ export enum LEGEND_TYPES { PROPORTION = 'proportion', } -export type LegendLayer = { +export type LegendLayerData = { id: string; title?: string; visible?: boolean; // layer visibility state @@ -20,12 +20,12 @@ export type LegendLayer = { helperText?: React.ReactNode; // note to show below all legend items minZoom?: number; // min zoom at which layer is displayed maxZoom?: number; // max zoom at which layer is displayed - legend?: LegendLayerVariable | LegendLayerVariable[]; + legend?: LegendLayerVariableData | LegendLayerVariableData[]; }; -export type LegendLayerVariable = { +export type LegendLayerVariableData = { type: LEGEND_TYPES; - select: LegendItemSelectConfig + select: LegendSelectConfig attr?: React.ReactNode; // subtitle to show below the legend item toggle when expanded } & LegendType; @@ -54,7 +54,7 @@ type LegendProportion = { labels: [number, number] } -export type LegendItemSelectConfig<T = unknown> = { +export type LegendSelectConfig<T = unknown> = { label: string; value: T; options: { @@ -64,6 +64,6 @@ export type LegendItemSelectConfig<T = unknown> = { }; export type CustomLegendComponent = React.ComponentType<{ - layer: LegendLayer; - legend: LegendLayerVariable; + layer: LegendLayerData; + legend: LegendLayerVariableData; }>; diff --git a/packages/react-ui/src/widgets/new-legend/LegendItem.js b/packages/react-ui/src/widgets/new-legend/LegendLayer.js similarity index 68% rename from packages/react-ui/src/widgets/new-legend/LegendItem.js rename to packages/react-ui/src/widgets/new-legend/LegendLayer.js index fa2b85171..8a4542de4 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendItem.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayer.js @@ -1,19 +1,14 @@ import PropTypes from 'prop-types'; import { Box, Collapse, IconButton, Tooltip, Typography } from '@mui/material'; -import { - LEGEND_TYPES, - LegendCategories, - LegendIcon, - LegendProportion, - LegendRamp -} from '@carto/react-ui'; import EyeIcon from '@mui/icons-material/VisibilityOutlined'; import EyeOffIcon from '@mui/icons-material/VisibilityOffOutlined'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; -import { useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useMemo, useRef, useState } from 'react'; import { styles } from './LegendWidgetUI.styles'; import LegendOpacityControl from './LegendOpacityControl'; +import LegendLayerTitle from './LegendLayerTitle'; +import LegendLayerVariable from './LegendLayerVariable'; const EMPTY_OBJ = {}; @@ -21,7 +16,7 @@ const EMPTY_OBJ = {}; * Receives configuration options, send change events and renders a legend item * @param {object} props * @param {Object.<string, import('../legend/LegendWidgetUI').CustomLegendComponent>} props.customLegendTypes - Allow to customise by default legend types that can be rendered. - * @param {import('../legend/LegendWidgetUI').LegendLayer} props.layer - Layer object from redux store. + * @param {import('../legend/LegendWidgetUI').LegendLayerData} props.layer - Layer object from redux store. * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} props.onChangeCollapsed - Callback function for layer visibility change. * @param {({ id, opacity }: { id: string, opacity: number }) => void} props.onChangeOpacity - Callback function for layer opacity change. * @param {({ id, visible }: { id: string, visible: boolean }) => void} props.onChangeVisibility - Callback function for layer collapsed state change. @@ -31,7 +26,7 @@ const EMPTY_OBJ = {}; * @param {number} props.currentZoom - Current zoom level for the map. * @returns {React.ReactNode} */ -export default function LegendItem({ +export default function LegendLayer({ customLegendTypes = EMPTY_OBJ, layer = EMPTY_OBJ, onChangeCollapsed, @@ -43,27 +38,20 @@ export default function LegendItem({ currentZoom }) { // layer legend defaults as defined here: https://docs.carto.com/carto-for-developers/carto-for-react/library-reference/widgets#legendwidget - const id = layer?.id; - const title = layer?.title; - const visible = layer?.visible ?? true; + const id = layer.id; + const title = layer.title; + const visible = layer.visible ?? true; const switchable = layer.switchable ?? true; const collapsed = layer.collapsed ?? false; const collapsible = layer.collapsible ?? true; const opacity = layer.opacity ?? 1; const showOpacityControl = layer.showOpacityControl ?? true; - - const legendItemVariables = useMemo(() => { - if (!layer.legend) { - return []; - } - return Array.isArray(layer.legend) ? layer.legend : [layer.legend]; - }, [layer.legend]); - const isExpanded = visible && !collapsed; const collapseIcon = isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />; const [opacityOpen, setOpacityOpen] = useState(false); const menuAnchorRef = useRef(null); + const layerHasZoom = layer?.minZoom !== undefined || layer?.maxZoom !== undefined; const showZoomNote = layerHasZoom && (layer.minZoom > minZoom || layer.maxZoom < maxZoom); @@ -77,9 +65,12 @@ export default function LegendItem({ }); const helperText = layer.helperText ?? showZoomNote ? zoomHelperText : ''; - if (!layer) { - return null; - } + const legendLayerVariables = useMemo(() => { + if (!layer.legend) { + return []; + } + return Array.isArray(layer.legend) ? layer.legend : [layer.legend]; + }, [layer.legend]); return ( <Box @@ -103,7 +94,7 @@ export default function LegendItem({ </IconButton> )} <Box flexGrow={1} sx={{ minWidth: 0, flexShrink: 1 }}> - <LegendItemTitle visible={visible} title={title} /> + <LegendLayerTitle visible={visible} title={title} /> {showZoomNote && ( <Typography color={visible ? 'textPrimary' : 'textSecondary'} @@ -142,8 +133,8 @@ export default function LegendItem({ </Box> <Collapse unmountOnExit timeout={100} sx={styles.legendItemBody} in={isExpanded}> <Box pb={2} opacity={outsideCurrentZoom ? 0.5 : 1}> - {legendItemVariables.map((legend) => ( - <LegendItemVariable + {legendLayerVariables.map((legend) => ( + <LegendLayerVariable key={legend.type} legend={legend} layer={layer} @@ -167,7 +158,7 @@ export default function LegendItem({ ); } -LegendItem.propTypes = { +LegendLayer.propTypes = { customLegendTypes: PropTypes.object.isRequired, layer: PropTypes.object.isRequired, onChangeCollapsed: PropTypes.func.isRequired, @@ -178,7 +169,7 @@ LegendItem.propTypes = { minZoom: PropTypes.number, currentZoom: PropTypes.number }; -LegendItem.defaultProps = { +LegendLayer.defaultProps = { maxZoom: 21, minZoom: 0, currentZoom: 0 @@ -198,83 +189,3 @@ function getZoomHelperText({ minZoom, maxZoom, layerMinZoom, layerMaxZoom }) { const texts = [maxZoomText, minZoomText].filter(Boolean).join(' and '); return texts ? `Note: this layer will display at zoom levels ${texts}` : ''; } - -/** - * @param {object} props - * @param {string} props.title - * @param {boolean} props.visible - * @returns {React.ReactNode} - */ -function LegendItemTitle({ title, visible }) { - const ref = useRef(null); - const [isOverflow, setIsOverflow] = useState(false); - - useLayoutEffect(() => { - if (visible && ref.current) { - const { offsetWidth, scrollWidth } = ref.current; - setIsOverflow(offsetWidth < scrollWidth); - } - }, [title, visible]); - - const element = ( - <Typography - ref={ref} - color={visible ? 'textPrimary' : 'textSecondary'} - variant='button' - fontWeight={500} - lineHeight='20px' - component='p' - noWrap - sx={{ my: 0.25 }} - > - {title} - </Typography> - ); - - if (!isOverflow) { - return element; - } - - return <Tooltip title={title}>{element}</Tooltip>; -} - -const legendTypeMap = { - [LEGEND_TYPES.CATEGORY]: LegendCategories, - [LEGEND_TYPES.ICON]: LegendIcon, - [LEGEND_TYPES.BINS]: LegendRamp, - [LEGEND_TYPES.PROPORTION]: LegendProportion, - [LEGEND_TYPES.CONTINUOUS_RAMP]: LegendRamp -}; - -/** - * @param {object} props - * @param {import('../legend/LegendWidgetUI').LegendLayerVariable} props.legend - legend variable data. - * @returns {React.ReactNode} - */ -function LegendUnknown({ legend }) { - return ( - <Typography variant='body2' color='textSecondary' component='p'> - Legend type {legend.type} not supported - </Typography> - ); -} - -/** - * @param {object} props - * @param {import('../legend/LegendWidgetUI').LegendLayer} props.layer - Layer object from redux store. - * @param {import('../legend/LegendWidgetUI').LegendLayerVariable} props.legend - legend variable data. - * @param {Object.<string, import('../legend/LegendWidgetUI').CustomLegendComponent>} props.customLegendTypes - Map from legend type to legend component that allows to customise additional legend types that can be rendered. - * @param {({ id, selection }: { id: string, selection: unknown }) => void} props.onChangeSelection - Callback function for legend options change. - * @returns {React.ReactNode} - */ -function LegendItemVariable({ layer, legend, customLegendTypes, onChangeSelection }) { - const type = legend.type; - const TypeComponent = legendTypeMap[type] || customLegendTypes[type] || LegendUnknown; - - return ( - <> - <div id='legend-option-selector'></div> - <TypeComponent layer={layer} legend={legend} /> - </> - ); -} diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js b/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js new file mode 100644 index 000000000..92bb6bec5 --- /dev/null +++ b/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js @@ -0,0 +1,41 @@ +import { Tooltip, Typography } from '@mui/material'; +import { useLayoutEffect, useRef, useState } from 'react'; + +/** Renders the legend layer title with an optional tooltip if the title is detected to be too long. + * @param {object} props + * @param {string} props.title + * @param {boolean} props.visible + * @returns {React.ReactNode} + */ +export default function LegendLayerTitle({ title, visible }) { + const ref = useRef(null); + const [isOverflow, setIsOverflow] = useState(false); + + useLayoutEffect(() => { + if (visible && ref.current) { + const { offsetWidth, scrollWidth } = ref.current; + setIsOverflow(offsetWidth < scrollWidth); + } + }, [title, visible]); + + const element = ( + <Typography + ref={ref} + color={visible ? 'textPrimary' : 'textSecondary'} + variant='button' + fontWeight={500} + lineHeight='20px' + component='p' + noWrap + sx={{ my: 0.25 }} + > + {title} + </Typography> + ); + + if (!isOverflow) { + return element; + } + + return <Tooltip title={title}>{element}</Tooltip>; +} diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js new file mode 100644 index 000000000..92ec0927d --- /dev/null +++ b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js @@ -0,0 +1,52 @@ +import { Typography } from '@mui/material'; +import LegendCategories from '../legend/LegendCategories'; +import LegendIcon from '../legend/LegendIcon'; +import LegendRamp from '../legend/LegendRamp'; +import LegendProportion from '../legend/LegendProportion'; +import { LEGEND_TYPES } from '../legend/LegendWidgetUI'; + +const legendTypeMap = { + [LEGEND_TYPES.CATEGORY]: LegendCategories, + [LEGEND_TYPES.ICON]: LegendIcon, + [LEGEND_TYPES.BINS]: LegendRamp, + [LEGEND_TYPES.PROPORTION]: LegendProportion, + [LEGEND_TYPES.CONTINUOUS_RAMP]: LegendRamp +}; + +/** + * @param {object} props + * @param {import('../legend/LegendWidgetUI').LegendLayerVariableData} props.legend - legend variable data. + * @returns {React.ReactNode} + */ +function LegendUnknown({ legend }) { + return ( + <Typography variant='body2' color='textSecondary' component='p'> + Legend type {legend.type} not supported + </Typography> + ); +} + +/** + * @param {object} props + * @param {import('../legend/LegendWidgetUI').LegendLayerData} props.layer - Layer object from redux store. + * @param {import('../legend/LegendWidgetUI').LegendLayerVariableData} props.legend - legend variable data. + * @param {Object.<string, import('../legend/LegendWidgetUI').CustomLegendComponent>} props.customLegendTypes - Map from legend type to legend component that allows to customise additional legend types that can be rendered. + * @param {({ id, selection }: { id: string, selection: unknown }) => void} props.onChangeSelection - Callback function for legend options change. + * @returns {React.ReactNode} + */ +export default function LegendLayerVariable({ + layer, + legend, + customLegendTypes, + onChangeSelection +}) { + const type = legend.type; + const TypeComponent = legendTypeMap[type] || customLegendTypes[type] || LegendUnknown; + + return ( + <> + <div id='legend-option-selector'></div> + <TypeComponent layer={layer} legend={legend} /> + </> + ); +} diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js index 3b4a34924..2caa6aa8d 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js @@ -3,7 +3,7 @@ import { Box, Collapse, IconButton, Paper, Tooltip, Typography } from '@mui/mate import CloseIcon from '@mui/icons-material/Close'; import LayerIcon from '@mui/icons-material/LayersOutlined'; import { LEGEND_WIDTH, styles } from './LegendWidgetUI.styles'; -import LegendItem from './LegendItem'; +import LegendLayer from './LegendLayer'; const EMPTY_OBJ = {}; const EMPTY_FN = () => {}; @@ -12,7 +12,7 @@ const EMPTY_ARR = []; /** * @param {object} props * @param {Object.<string, import('../legend/LegendWidgetUI').CustomLegendComponent>} [props.customLegendTypes] - Allow to customise by default legend types that can be rendered. - * @param {import('../legend/LegendWidgetUI').LegendLayer[]} [props.layers] - Array of layer objects from redux store. + * @param {import('../legend/LegendWidgetUI').LegendLayerData[]} [props.layers] - Array of layer objects from redux store. * @param {boolean} [props.collapsed] - Collapsed state for whole legend widget. * @param {(collapsed: boolean) => void} props.onChangeCollapsed - Callback function for collapsed state change. * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} props.onChangeLegendRowCollapsed - Callback function for layer visibility change. @@ -75,7 +75,7 @@ function NewLegendWidgetUI({ <Box sx={{ ...styles.legendItemList, width: collapsed ? 0 : undefined }}> <Collapse unmountOnExit in={!collapsed} timeout={500}> {layers.map((l) => ( - <LegendItem + <LegendLayer key={l.id} layer={l} onChangeCollapsed={onChangeLegendRowCollapsed} From 0f29e5d1ba29b403bf9cf65839dbc3cc00ca70f5 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Tue, 6 Feb 2024 18:31:43 +0100 Subject: [PATCH 16/66] option selector WIP --- .../src/widgets/new-legend/LegendLayer.js | 8 +++-- .../widgets/new-legend/LegendLayerVariable.js | 34 +++++++++++++++++-- .../src/widgets/new-legend/LegendWidgetUI.js | 4 +++ .../stories/widgetsUI/legendFixtures.js | 16 +++++++++ 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayer.js b/packages/react-ui/src/widgets/new-legend/LegendLayer.js index 8a4542de4..622a1688e 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayer.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayer.js @@ -20,7 +20,7 @@ const EMPTY_OBJ = {}; * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} props.onChangeCollapsed - Callback function for layer visibility change. * @param {({ id, opacity }: { id: string, opacity: number }) => void} props.onChangeOpacity - Callback function for layer opacity change. * @param {({ id, visible }: { id: string, visible: boolean }) => void} props.onChangeVisibility - Callback function for layer collapsed state change. - * @param {({ id, selection }: { id: string, selection: unknown }) => void} props.onChangeSelection - Callback function for layer selection change. + * @param {({ id, index, selection }: { id: string, index: number, selection: unknown }) => void} props.onChangeSelection - Callback function for layer selection change. * @param {number} props.maxZoom - Global maximum zoom level for the map. * @param {number} props.minZoom - Global minimum zoom level for the map. * @param {number} props.currentZoom - Current zoom level for the map. @@ -133,13 +133,15 @@ export default function LegendLayer({ </Box> <Collapse unmountOnExit timeout={100} sx={styles.legendItemBody} in={isExpanded}> <Box pb={2} opacity={outsideCurrentZoom ? 0.5 : 1}> - {legendLayerVariables.map((legend) => ( + {legendLayerVariables.map((legend, index) => ( <LegendLayerVariable key={legend.type} legend={legend} layer={layer} customLegendTypes={customLegendTypes} - onChangeSelection={(selection) => onChangeSelection({ id, selection })} + onChangeSelection={(selection) => + onChangeSelection({ id, index, selection }) + } /> ))} </Box> diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js index 92ec0927d..386e3da23 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js @@ -1,4 +1,4 @@ -import { Typography } from '@mui/material'; +import { Box, MenuItem, OutlinedInput, Select, Typography } from '@mui/material'; import LegendCategories from '../legend/LegendCategories'; import LegendIcon from '../legend/LegendIcon'; import LegendRamp from '../legend/LegendRamp'; @@ -31,7 +31,7 @@ function LegendUnknown({ legend }) { * @param {import('../legend/LegendWidgetUI').LegendLayerData} props.layer - Layer object from redux store. * @param {import('../legend/LegendWidgetUI').LegendLayerVariableData} props.legend - legend variable data. * @param {Object.<string, import('../legend/LegendWidgetUI').CustomLegendComponent>} props.customLegendTypes - Map from legend type to legend component that allows to customise additional legend types that can be rendered. - * @param {({ id, selection }: { id: string, selection: unknown }) => void} props.onChangeSelection - Callback function for legend options change. + * @param {(selection: unknown) => void} props.onChangeSelection - Callback function for legend options change. * @returns {React.ReactNode} */ export default function LegendLayerVariable({ @@ -42,10 +42,38 @@ export default function LegendLayerVariable({ }) { const type = legend.type; const TypeComponent = legendTypeMap[type] || customLegendTypes[type] || LegendUnknown; + const selectOptions = legend.select?.options || []; return ( <> - <div id='legend-option-selector'></div> + {legend.select ? ( + <Box> + <Typography variant='caption'>Basemap style</Typography> + <Select + value={legend.select.value} + renderValue={() => + selectOptions.find((opt) => opt.id === legend.select.value)?.label + } + onChange={(ev) => onChangeSelection(ev.target.value)} + input={<OutlinedInput />} + MenuProps={{ + transformOrigin: { vertical: 'bottom', horizontal: 'left' }, + anchorOrigin: { vertical: 'top', horizontal: 'left' }, + PaperProps: { + style: { + maxHeight: 240 + } + } + }} + > + {selectOptions.map((option) => ( + <MenuItem key={option.value} value={option.value}> + {option.label} + </MenuItem> + ))} + </Select> + </Box> + ) : null} <TypeComponent layer={layer} legend={legend} /> </> ); diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js index 2caa6aa8d..e64ec1fbb 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js @@ -18,6 +18,7 @@ const EMPTY_ARR = []; * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} props.onChangeLegendRowCollapsed - Callback function for layer visibility change. * @param {({ id, opacity }: { id: string, opacity: number }) => void} props.onChangeOpacity - Callback function for layer opacity change. * @param {({ id, visible }: { id: string, visible: boolean }) => void} props.onChangeVisibility - Callback function for layer collapsed state change. + * @param {({ id, index, selection }: { id: string, index: number, selection: unknown }) => void} props.onChangeSelection - Callback function for layer variable selection change. * @param {string[]} [props.layerOrder] - Array of layer identifiers. Defines the order of layer legends. [] by default. * @param {string} [props.title] - Title of the toggle button when widget is open. * @param {'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'} [props.position] - Position of the widget. @@ -34,6 +35,7 @@ function NewLegendWidgetUI({ onChangeVisibility = EMPTY_FN, onChangeOpacity = EMPTY_FN, onChangeLegendRowCollapsed = EMPTY_FN, + onChangeSelection = EMPTY_FN, layerOrder, title, position = 'bottom-right', @@ -81,6 +83,7 @@ function NewLegendWidgetUI({ onChangeCollapsed={onChangeLegendRowCollapsed} onChangeOpacity={onChangeOpacity} onChangeVisibility={onChangeVisibility} + onChangeSelection={onChangeSelection} maxZoom={maxZoom} minZoom={minZoom} currentZoom={currentZoom} @@ -109,6 +112,7 @@ NewLegendWidgetUI.propTypes = { onChangeLegendRowCollapsed: PropTypes.func.isRequired, onChangeVisibility: PropTypes.func.isRequired, onChangeOpacity: PropTypes.func.isRequired, + onChangeSelection: PropTypes.func.isRequired, layerOrder: PropTypes.arrayOf(PropTypes.string), title: PropTypes.string, position: PropTypes.oneOf(['top-left', 'top-right', 'bottom-left', 'bottom-right']) diff --git a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js index 3f9822443..b0912ddb6 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js +++ b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js @@ -21,6 +21,22 @@ export const fixtures = [ labels: ['Applicants'] } }, + { + id: 'basemap', + title: 'Basemap', + legend: { + type: 'basemap', + collapsible: false, + select: { + label: 'Select basemap', + value: 'light', + options: [ + { label: 'Light', value: 'light' }, + { label: 'Dark', value: 'dark' } + ] + } + } + }, { id: 'avland', title: 'Available Land', From a09426e06ef3b63ffebbd59d5790d6f05c201bc3 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Tue, 6 Feb 2024 18:51:23 +0100 Subject: [PATCH 17/66] finish layer selector --- .../src/widgets/new-legend/LegendLayerVariable.js | 15 +++++++++------ .../widgets/new-legend/LegendWidgetUI.styles.js | 1 + .../storybook/stories/widgetsUI/legendFixtures.js | 4 +++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js index 386e3da23..9124fe151 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js @@ -1,4 +1,4 @@ -import { Box, MenuItem, OutlinedInput, Select, Typography } from '@mui/material'; +import { Box, ListItemText, MenuItem, Select, Typography } from '@mui/material'; import LegendCategories from '../legend/LegendCategories'; import LegendIcon from '../legend/LegendIcon'; import LegendRamp from '../legend/LegendRamp'; @@ -19,6 +19,10 @@ const legendTypeMap = { * @returns {React.ReactNode} */ function LegendUnknown({ legend }) { + if (legend.select) { + return null; + } + return ( <Typography variant='body2' color='textSecondary' component='p'> Legend type {legend.type} not supported @@ -48,14 +52,13 @@ export default function LegendLayerVariable({ <> {legend.select ? ( <Box> - <Typography variant='caption'>Basemap style</Typography> + <Typography variant='caption'>{legend.select.label}</Typography> <Select value={legend.select.value} - renderValue={() => - selectOptions.find((opt) => opt.id === legend.select.value)?.label + renderValue={(value) => + selectOptions.find((option) => option.value === value)?.label || value } onChange={(ev) => onChangeSelection(ev.target.value)} - input={<OutlinedInput />} MenuProps={{ transformOrigin: { vertical: 'bottom', horizontal: 'left' }, anchorOrigin: { vertical: 'top', horizontal: 'left' }, @@ -68,7 +71,7 @@ export default function LegendLayerVariable({ > {selectOptions.map((option) => ( <MenuItem key={option.value} value={option.value}> - {option.label} + <ListItemText primary={option.label} /> </MenuItem> ))} </Select> diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js index 26a7c3958..da2c5d324 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js @@ -34,6 +34,7 @@ export const styles = { display: 'flex', justifyContent: 'space-between', position: 'sticky', + zIndex: 2, top: 0, background: (theme) => theme.palette.background.paper }, diff --git a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js index b0912ddb6..00328330b 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js +++ b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js @@ -24,9 +24,11 @@ export const fixtures = [ { id: 'basemap', title: 'Basemap', + collapsible: true, + switchable: false, + showOpacityControl: false, legend: { type: 'basemap', - collapsible: false, select: { label: 'Select basemap', value: 'light', From 4e3b78d0ad88435864c7b339ee810215f2bcc061 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Tue, 6 Feb 2024 18:56:40 +0100 Subject: [PATCH 18/66] translate visibility button --- .../src/widgets/new-legend/LegendLayer.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayer.js b/packages/react-ui/src/widgets/new-legend/LegendLayer.js index 622a1688e..a48a43914 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayer.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayer.js @@ -9,6 +9,8 @@ import { styles } from './LegendWidgetUI.styles'; import LegendOpacityControl from './LegendOpacityControl'; import LegendLayerTitle from './LegendLayerTitle'; import LegendLayerVariable from './LegendLayerVariable'; +import { useIntl } from 'react-intl'; +import useImperativeIntl from '@carto/react-ui/hooks/useImperativeIntl'; const EMPTY_OBJ = {}; @@ -37,6 +39,11 @@ export default function LegendLayer({ minZoom, currentZoom }) { + const intl = useIntl(); + const intlConfig = useImperativeIntl(intl); + const menuAnchorRef = useRef(null); + const [opacityOpen, setOpacityOpen] = useState(false); + // layer legend defaults as defined here: https://docs.carto.com/carto-for-developers/carto-for-react/library-reference/widgets#legendwidget const id = layer.id; const title = layer.title; @@ -49,9 +56,6 @@ export default function LegendLayer({ const isExpanded = visible && !collapsed; const collapseIcon = isExpanded ? <ExpandLessIcon /> : <ExpandMoreIcon />; - const [opacityOpen, setOpacityOpen] = useState(false); - const menuAnchorRef = useRef(null); - const layerHasZoom = layer?.minZoom !== undefined || layer?.maxZoom !== undefined; const showZoomNote = layerHasZoom && (layer.minZoom > minZoom || layer.maxZoom < maxZoom); @@ -115,7 +119,13 @@ export default function LegendLayer({ /> )} {switchable && ( - <Tooltip title={visible ? 'Hide layer' : 'Show layer'}> + <Tooltip + title={intlConfig.formatMessage({ + id: visible + ? 'c4r.widgets.legend.hideLayer' + : 'c4r.widgets.legend.showLayer' + })} + > <IconButton size='small' onClick={() => From d84416a86367a982c53505ab12d65d0d3a2a108f Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Tue, 6 Feb 2024 18:58:09 +0100 Subject: [PATCH 19/66] fix import --- packages/react-ui/src/widgets/new-legend/LegendLayer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayer.js b/packages/react-ui/src/widgets/new-legend/LegendLayer.js index a48a43914..3d254518a 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayer.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayer.js @@ -10,7 +10,7 @@ import LegendOpacityControl from './LegendOpacityControl'; import LegendLayerTitle from './LegendLayerTitle'; import LegendLayerVariable from './LegendLayerVariable'; import { useIntl } from 'react-intl'; -import useImperativeIntl from '@carto/react-ui/hooks/useImperativeIntl'; +import useImperativeIntl from '../../hooks/useImperativeIntl'; const EMPTY_OBJ = {}; From b2491d76d78e9dff53a06aee0697d9d3ba14efeb Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Wed, 7 Feb 2024 10:48:47 +0100 Subject: [PATCH 20/66] translate more layers --- packages/react-ui/src/localization/en.js | 6 +++++- packages/react-ui/src/widgets/new-legend/LegendLayer.js | 4 +++- .../react-ui/src/widgets/new-legend/LegendWidgetUI.js | 9 +++++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/react-ui/src/localization/en.js b/packages/react-ui/src/localization/en.js index 3a9ec15a9..48a705d52 100644 --- a/packages/react-ui/src/localization/en.js +++ b/packages/react-ui/src/localization/en.js @@ -39,7 +39,11 @@ const locales = { layer: 'layer', opacity: 'Opacity', hideLayer: 'Hide layer', - showLayer: 'Show layer' + showLayer: 'Show layer', + open: 'Open legend', + close: 'Close', + collapse: 'Collapse layer', + expand: 'Expand layer' }, range: { clear: 'Clear', diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayer.js b/packages/react-ui/src/widgets/new-legend/LegendLayer.js index 3d254518a..05cad84db 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayer.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayer.js @@ -90,7 +90,9 @@ export default function LegendLayer({ {collapsible && ( <IconButton size='small' - aria-label='Toggle legend item collapsed' + aria-label={intlConfig.formatMessage({ + id: collapsed ? 'c4r.widgets.legend.expand' : 'c4r.widgets.legend.collapse' + })} disabled={!visible} onClick={() => onChangeCollapsed({ id, collapsed: !collapsed })} > diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js index e64ec1fbb..40f1fa85b 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js @@ -4,6 +4,8 @@ import CloseIcon from '@mui/icons-material/Close'; import LayerIcon from '@mui/icons-material/LayersOutlined'; import { LEGEND_WIDTH, styles } from './LegendWidgetUI.styles'; import LegendLayer from './LegendLayer'; +import { useIntl } from 'react-intl'; +import useImperativeIntl from '../../hooks/useImperativeIntl'; const EMPTY_OBJ = {}; const EMPTY_FN = () => {}; @@ -43,6 +45,9 @@ function NewLegendWidgetUI({ minZoom = 0, currentZoom } = {}) { + const intl = useIntl(); + const intlConfig = useImperativeIntl(intl); + const rootSx = { ...styles[position], ...styles.root, @@ -52,7 +57,7 @@ function NewLegendWidgetUI({ return ( <Paper elevation={3} sx={rootSx}> {collapsed ? ( - <Tooltip title='Open legend'> + <Tooltip title={intlConfig.formatMessage({ id: 'c4r.widgets.legend.open' })}> <IconButton onClick={() => onChangeCollapsed(false)}> <LayerIcon /> </IconButton> @@ -67,7 +72,7 @@ function NewLegendWidgetUI({ <Typography variant='caption' sx={{ flexGrow: 1 }}> {title} </Typography> - <Tooltip title='Close'> + <Tooltip title={intlConfig.formatMessage({ id: 'c4r.widgets.legend.close' })}> <IconButton size='small' onClick={() => onChangeCollapsed(true)}> <CloseIcon /> </IconButton> From d062ecc61694f3cfeddf2f1c469641fd5eee9347 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Wed, 7 Feb 2024 11:04:04 +0100 Subject: [PATCH 21/66] translate all labels --- packages/react-ui/src/localization/en.js | 8 +++++++- .../src/widgets/new-legend/LegendLayer.js | 20 +++++++++++++------ .../widgets/new-legend/LegendLayerVariable.js | 8 +++++++- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/react-ui/src/localization/en.js b/packages/react-ui/src/localization/en.js index 48a705d52..0ec12ba9a 100644 --- a/packages/react-ui/src/localization/en.js +++ b/packages/react-ui/src/localization/en.js @@ -43,7 +43,13 @@ const locales = { open: 'Open legend', close: 'Close', collapse: 'Collapse layer', - expand: 'Expand layer' + expand: 'Expand layer', + zoomLevel: 'Zoom level', + lowerThan: 'lower than', + greaterThan: 'greater than', + and: 'and', + zoomNote: 'Note: this layer will display at zoom levels', + notSupported: 'legend type not supported' }, range: { clear: 'Clear', diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayer.js b/packages/react-ui/src/widgets/new-legend/LegendLayer.js index 05cad84db..2ab48ae2c 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayer.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayer.js @@ -62,6 +62,7 @@ export default function LegendLayer({ const outsideCurrentZoom = currentZoom < layer.minZoom || currentZoom > layer.maxZoom; const zoomHelperText = getZoomHelperText({ + intl: intlConfig, minZoom, maxZoom, layerMinZoom: layer.minZoom, @@ -107,7 +108,8 @@ export default function LegendLayer({ variant='caption' component='p' > - Zoom level: {layer.minZoom} - {layer.maxZoom} + {intlConfig.formatMessage({ id: 'c4r.widgets.legend.zoomLevel' })}{' '} + {layer.minZoom} - {layer.maxZoom} </Typography> )} </Box> @@ -191,15 +193,21 @@ LegendLayer.defaultProps = { /** * @param {object} props + * @param {import('react-intl').IntlShape} props.intl - React Intl object. * @param {number} props.minZoom - Global minimum zoom level for the map. * @param {number} props.maxZoom - Global maximum zoom level for the map. * @param {number} props.layerMinZoom - Layer minimum zoom level. * @param {number} props.layerMaxZoom - Layer maximum zoom level. * @returns {string} */ -function getZoomHelperText({ minZoom, maxZoom, layerMinZoom, layerMaxZoom }) { - const maxZoomText = layerMaxZoom < maxZoom ? `lower than ${layerMaxZoom}` : ''; - const minZoomText = layerMinZoom > minZoom ? `greater than ${layerMinZoom}` : ''; - const texts = [maxZoomText, minZoomText].filter(Boolean).join(' and '); - return texts ? `Note: this layer will display at zoom levels ${texts}` : ''; +function getZoomHelperText({ intl, minZoom, maxZoom, layerMinZoom, layerMaxZoom }) { + const and = intl.formatMessage({ id: 'c4r.widgets.legend.and' }); + const lowerThan = intl.formatMessage({ id: 'c4r.widgets.legend.lowerThan' }); + const greaterThan = intl.formatMessage({ id: 'c4r.widgets.legend.greaterThan' }); + const note = intl.formatMessage({ id: 'c4r.widgets.legend.zoomNote' }); + + const maxZoomText = layerMaxZoom < maxZoom ? `${lowerThan} ${layerMaxZoom}` : ''; + const minZoomText = layerMinZoom > minZoom ? `${greaterThan} ${layerMinZoom}` : ''; + const texts = [maxZoomText, minZoomText].filter(Boolean).join(` ${and} `); + return texts ? `${note} ${texts}` : ''; } diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js index 9124fe151..b9f2bdc7e 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js @@ -4,6 +4,8 @@ import LegendIcon from '../legend/LegendIcon'; import LegendRamp from '../legend/LegendRamp'; import LegendProportion from '../legend/LegendProportion'; import { LEGEND_TYPES } from '../legend/LegendWidgetUI'; +import { useIntl } from 'react-intl'; +import useImperativeIntl from '../../hooks/useImperativeIntl'; const legendTypeMap = { [LEGEND_TYPES.CATEGORY]: LegendCategories, @@ -19,13 +21,17 @@ const legendTypeMap = { * @returns {React.ReactNode} */ function LegendUnknown({ legend }) { + const intl = useIntl(); + const intlConfig = useImperativeIntl(intl); + if (legend.select) { return null; } return ( <Typography variant='body2' color='textSecondary' component='p'> - Legend type {legend.type} not supported + "{legend.type}"{' '} + {intlConfig.formatMessage({ id: 'c4r.widgets.legend.notSupported' })} </Typography> ); } From dee933e6f8b0f4dd039bc385a35b3108beef73dc Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Wed, 7 Feb 2024 11:19:16 +0100 Subject: [PATCH 22/66] extract opacity icon --- packages/react-ui/src/assets/icons/OpacityIcon.js | 13 +++++++++++++ .../widgets/new-legend/LegendOpacityControl.js | 15 ++------------- 2 files changed, 15 insertions(+), 13 deletions(-) create mode 100644 packages/react-ui/src/assets/icons/OpacityIcon.js diff --git a/packages/react-ui/src/assets/icons/OpacityIcon.js b/packages/react-ui/src/assets/icons/OpacityIcon.js new file mode 100644 index 000000000..73acca8a2 --- /dev/null +++ b/packages/react-ui/src/assets/icons/OpacityIcon.js @@ -0,0 +1,13 @@ +import { SvgIcon } from '@mui/material'; + +export default function OpacityIcon(props) { + return ( + <SvgIcon {...props}> + <path + fillRule='evenodd' + clipRule='evenodd' + d='M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM17.625 19V17.625H19V15.375H17.625V13.125H19V10.875H17.625V8.625H19V6.375H17.625V5H15.375V6.375H13.125V5H10.875V6.375H8.625V5H6.375V6.375H5V8.625H6.375V10.875H5V13.125H6.375V15.375H5V17.625H6.375V19H8.625V17.625H10.875V19H13.125V17.625H15.375V19H17.625ZM15.375 15.375H17.625V17.625H15.375V15.375ZM13.125 15.375H15.375V13.125H17.625V10.875H15.375V8.625H17.625V6.375H15.375V8.625H13.125V6.375H10.875V8.625H8.625V6.375H6.375V8.625H8.625V10.875H6.375V13.125H8.625V15.375H6.375V17.625H8.625V15.375H10.875V17.625H13.125V15.375ZM13.125 13.125H15.375V10.875H13.125V8.625H10.875V10.875H8.625V13.125H10.875V15.375H13.125V13.125ZM13.125 13.125H10.875V10.875H13.125V13.125Z' + /> + </SvgIcon> + ); +} diff --git a/packages/react-ui/src/widgets/new-legend/LegendOpacityControl.js b/packages/react-ui/src/widgets/new-legend/LegendOpacityControl.js index d1b1503e9..7c6b6da28 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendOpacityControl.js +++ b/packages/react-ui/src/widgets/new-legend/LegendOpacityControl.js @@ -10,6 +10,7 @@ import { import { styles } from './LegendWidgetUI.styles'; import { useIntl } from 'react-intl'; import useImperativeIntl from '../../hooks/useImperativeIntl'; +import OpacityIcon from '../../assets/icons/OpacityIcon'; /** * @param {object} props @@ -44,19 +45,7 @@ export default function LegendOpacityControl({ color={open ? 'primary' : 'default'} onClick={() => toggleOpen(!open)} > - <svg - width='24' - height='24' - viewBox='0 0 24 24' - fill='none' - xmlns='http://www.w3.org/2000/svg' - > - <path - fillRule='evenodd' - clipRule='evenodd' - d='M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM17.625 19V17.625H19V15.375H17.625V13.125H19V10.875H17.625V8.625H19V6.375H17.625V5H15.375V6.375H13.125V5H10.875V6.375H8.625V5H6.375V6.375H5V8.625H6.375V10.875H5V13.125H6.375V15.375H5V17.625H6.375V19H8.625V17.625H10.875V19H13.125V17.625H15.375V19H17.625ZM15.375 15.375H17.625V17.625H15.375V15.375ZM13.125 15.375H15.375V13.125H17.625V10.875H15.375V8.625H17.625V6.375H15.375V8.625H13.125V6.375H10.875V8.625H8.625V6.375H6.375V8.625H8.625V10.875H6.375V13.125H8.625V15.375H6.375V17.625H8.625V15.375H10.875V17.625H13.125V15.375ZM13.125 13.125H15.375V10.875H13.125V8.625H10.875V10.875H8.625V13.125H10.875V15.375H13.125V13.125ZM13.125 13.125H10.875V10.875H13.125V13.125Z' - /> - </svg> + <OpacityIcon /> </IconButton> </Tooltip> <Popover From b1245ae85e80246a231776900b971568ad5fa464 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Wed, 7 Feb 2024 15:32:37 +0100 Subject: [PATCH 23/66] add more styles --- packages/react-ui/src/localization/en.js | 1 + .../src/widgets/new-legend/LegendLayer.js | 38 +++++++++++-------- .../widgets/new-legend/LegendLayerVariable.js | 6 +-- .../new-legend/LegendWidgetUI.styles.js | 5 +++ .../stories/widgetsUI/legendFixtures.js | 37 +++++++++--------- 5 files changed, 51 insertions(+), 36 deletions(-) diff --git a/packages/react-ui/src/localization/en.js b/packages/react-ui/src/localization/en.js index 0ec12ba9a..6684516d6 100644 --- a/packages/react-ui/src/localization/en.js +++ b/packages/react-ui/src/localization/en.js @@ -45,6 +45,7 @@ const locales = { collapse: 'Collapse layer', expand: 'Expand layer', zoomLevel: 'Zoom level', + zoomLevelTooltip: 'This layer is only visible at certain zoom levels', lowerThan: 'lower than', greaterThan: 'greater than', and: 'and', diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayer.js b/packages/react-ui/src/widgets/new-legend/LegendLayer.js index 2ab48ae2c..837b8992f 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayer.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayer.js @@ -79,6 +79,7 @@ export default function LegendLayer({ return ( <Box + data-testid='legend-layer' aria-label={title} component='section' sx={{ @@ -103,14 +104,20 @@ export default function LegendLayer({ <Box flexGrow={1} sx={{ minWidth: 0, flexShrink: 1 }}> <LegendLayerTitle visible={visible} title={title} /> {showZoomNote && ( - <Typography - color={visible ? 'textPrimary' : 'textSecondary'} - variant='caption' - component='p' + <Tooltip + title={intlConfig.formatMessage({ + id: 'c4r.widgets.legend.zoomLevelTooltip' + })} > - {intlConfig.formatMessage({ id: 'c4r.widgets.legend.zoomLevel' })}{' '} - {layer.minZoom} - {layer.maxZoom} - </Typography> + <Typography + color={visible ? 'textPrimary' : 'textSecondary'} + variant='caption' + component='p' + > + {intlConfig.formatMessage({ id: 'c4r.widgets.legend.zoomLevel' })}{' '} + {layer.minZoom} - {layer.maxZoom} + </Typography> + </Tooltip> )} </Box> {showOpacityControl && visible && ( @@ -145,8 +152,14 @@ export default function LegendLayer({ </Tooltip> )} </Box> - <Collapse unmountOnExit timeout={100} sx={styles.legendItemBody} in={isExpanded}> - <Box pb={2} opacity={outsideCurrentZoom ? 0.5 : 1}> + <Collapse unmountOnExit timeout={100} in={isExpanded}> + <Box + data-testid='legend-layer-variable-list' + sx={{ + ...styles.layerVariablesList, + opacity: outsideCurrentZoom ? 0.5 : 1 + }} + > {legendLayerVariables.map((legend, index) => ( <LegendLayerVariable key={legend.type} @@ -160,12 +173,7 @@ export default function LegendLayer({ ))} </Box> {helperText && ( - <Typography - variant='caption' - color='textSecondary' - component='p' - sx={{ py: 2 }} - > + <Typography variant='caption' color='textSecondary' component='p' sx={{ p: 2 }}> {helperText} </Typography> )} diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js index b9f2bdc7e..3e23f5252 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js @@ -55,9 +55,9 @@ export default function LegendLayerVariable({ const selectOptions = legend.select?.options || []; return ( - <> + <Box data-testid='legend-layer-variable' px={2}> {legend.select ? ( - <Box> + <Box pb={1}> <Typography variant='caption'>{legend.select.label}</Typography> <Select value={legend.select.value} @@ -84,6 +84,6 @@ export default function LegendLayerVariable({ </Box> ) : null} <TypeComponent layer={layer} legend={legend} /> - </> + </Box> ); } diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js index da2c5d324..ea0f4ae31 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js @@ -60,6 +60,11 @@ export const styles = { // } // } }, + layerVariablesList: { + display: 'flex', + flexDirection: 'column', + gap: 1 + }, opacityControl: { display: 'flex', gap: 2, diff --git a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js index 00328330b..937e8c065 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js +++ b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js @@ -1,6 +1,24 @@ import { LEGEND_TYPES } from '@carto/react-ui'; export const fixtures = [ + { + id: 'basemap', + title: 'Basemap', + collapsible: true, + switchable: false, + showOpacityControl: false, + legend: { + type: 'basemap', + select: { + label: 'Select basemap', + value: 'light', + options: [ + { label: 'Light', value: 'light' }, + { label: 'Dark', value: 'dark' } + ] + } + } + }, { id: 'applicants', title: 'Applicants', @@ -21,24 +39,6 @@ export const fixtures = [ labels: ['Applicants'] } }, - { - id: 'basemap', - title: 'Basemap', - collapsible: true, - switchable: false, - showOpacityControl: false, - legend: { - type: 'basemap', - select: { - label: 'Select basemap', - value: 'light', - options: [ - { label: 'Light', value: 'light' }, - { label: 'Dark', value: 'dark' } - ] - } - } - }, { id: 'avland', title: 'Available Land', @@ -158,6 +158,7 @@ export const fixtures = [ legend: { collapsed: false, type: LEGEND_TYPES.ICON, + attr: 'icon_category', icons: [ `data:image/svg+xml,<svg width="128" height="129" viewBox="0 0 128 129" fill="none" xmlns="http://www.w3.org/2000/svg"> <circle opacity="0.5" cx="64.0003" cy="64.2" r="53.3333" fill="white"/> From eed96c28b14aba747a71f8bc0946aff5af61b06b Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Wed, 7 Feb 2024 16:21:01 +0100 Subject: [PATCH 24/66] legend attr subtitles --- packages/react-ui/src/localization/en.js | 8 ++++- .../widgets/new-legend/LegendLayerVariable.js | 30 +++++++++++++++++++ .../stories/widgetsUI/legendFixtures.js | 1 + 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/react-ui/src/localization/en.js b/packages/react-ui/src/localization/en.js index 6684516d6..cc097805d 100644 --- a/packages/react-ui/src/localization/en.js +++ b/packages/react-ui/src/localization/en.js @@ -50,7 +50,13 @@ const locales = { greaterThan: 'greater than', and: 'and', zoomNote: 'Note: this layer will display at zoom levels', - notSupported: 'legend type not supported' + notSupported: 'legend type not supported', + subtitles: { + proportion: 'Radius range by', + icon: 'Icon based on', + strokeColor: 'Stroke color based on', + color: 'Color based on' + } }, range: { clear: 'Clear', diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js index 3e23f5252..31683fd52 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js @@ -36,6 +36,23 @@ function LegendUnknown({ legend }) { ); } +/** + * @param {import('../legend/LegendWidgetUI').LegendLayerVariableData} legend - legend variable data. + * @returns {string} + */ +function getLegendSubtitle(legend) { + if (legend.type === LEGEND_TYPES.PROPORTION) { + return 'c4r.widgets.legend.subtitles.proportion'; + } + if (legend.type === LEGEND_TYPES.ICON || !!legend.customMarkers) { + return 'c4r.widgets.legend.subtitles.icon'; + } + if (legend.isStrokeColor) { + return 'c4r.widgets.legend.subtitles.strokeColor'; + } + return 'c4r.widgets.legend.subtitles.color'; +} + /** * @param {object} props * @param {import('../legend/LegendWidgetUI').LegendLayerData} props.layer - Layer object from redux store. @@ -50,12 +67,25 @@ export default function LegendLayerVariable({ customLegendTypes, onChangeSelection }) { + const intl = useIntl(); + const intlConfig = useImperativeIntl(intl); + const type = legend.type; const TypeComponent = legendTypeMap[type] || customLegendTypes[type] || LegendUnknown; const selectOptions = legend.select?.options || []; return ( <Box data-testid='legend-layer-variable' px={2}> + {legend.attr ? ( + <Box pb={1}> + <Typography variant='overlineDelicate'> + {intlConfig.formatMessage({ id: getLegendSubtitle(legend) })} + </Typography> + <Typography variant='caption' component='p'> + {legend.attr} + </Typography> + </Box> + ) : null} {legend.select ? ( <Box pb={1}> <Typography variant='caption'>{legend.select.label}</Typography> diff --git a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js index 937e8c065..2b0a8bd5f 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js +++ b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js @@ -9,6 +9,7 @@ export const fixtures = [ showOpacityControl: false, legend: { type: 'basemap', + attr: 'attr', select: { label: 'Select basemap', value: 'light', From 45abf19dbc7a2fe44a22e1a0cd13ac25970d6555 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Wed, 7 Feb 2024 16:21:19 +0100 Subject: [PATCH 25/66] complete legend types typing --- packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts index b084f6d25..58551bd22 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts @@ -31,7 +31,7 @@ export type LegendLayerVariableData = { type LegendType = LegendBins | LegendRamp | LegendIcons | LegendCategories | LegendProportion; -type LegendColors = string | string[]; +type LegendColors = string | string[] | number[][]; type LegendNumericLabels = number[] | { label: string; value: number }[]; type LegendBins = { @@ -48,7 +48,10 @@ type LegendIcons = { } type LegendCategories = { colors: LegendColors - labels: string[] + labels: string[] | number[] + isStrokeColor?: boolean + customMarkers?: string | string[] + maskedMarkers?: boolean } type LegendProportion = { labels: [number, number] From 87a9b2aa272d24d7d381e955f8da43347fd022f3 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Wed, 7 Feb 2024 18:04:15 +0100 Subject: [PATCH 26/66] update individual legend styles --- .../src/widgets/legend/LegendCategories.js | 121 ++++++++---------- .../react-ui/src/widgets/legend/LegendIcon.js | 46 ++++--- .../widgets/new-legend/LegendLayerTitle.js | 4 +- 3 files changed, 81 insertions(+), 90 deletions(-) diff --git a/packages/react-ui/src/widgets/legend/LegendCategories.js b/packages/react-ui/src/widgets/legend/LegendCategories.js index 10ef61be4..08744d34d 100644 --- a/packages/react-ui/src/widgets/legend/LegendCategories.js +++ b/packages/react-ui/src/widgets/legend/LegendCategories.js @@ -1,8 +1,8 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { Box, Grid, Tooltip, styled } from '@mui/material'; +import React from 'react'; +import { Box, Tooltip, styled } from '@mui/material'; import { getPalette } from '../../utils/palette'; import PropTypes from 'prop-types'; -import Typography from '../../components/atoms/Typography'; +import LegendLayerTitle from '../new-legend/LegendLayerTitle'; function LegendCategories({ legend }) { const { @@ -15,24 +15,28 @@ function LegendCategories({ legend }) { const palette = getPalette(colors, labels.length); - const Rows = labels.map((label, idx) => ( - <Row - key={label + idx} - isMax={false} - label={label} - color={palette[idx]} - icon={ - customMarkers && Array.isArray(customMarkers) ? customMarkers[idx] : customMarkers - } - maskedIcon={maskedMarkers} - isStrokeColor={isStrokeColor} - /> - )); - return ( - <Grid container direction='column' spacing={1} data-testid='categories-legend'> - {Rows} - </Grid> + <Box + component='ul' + data-testid='categories-legend' + sx={{ m: 0, p: 0, pb: 1, display: 'flex', flexDirection: 'column' }} + > + {labels.map((label, idx) => ( + <LegendCategoriesRow + key={label + idx} + isMax={false} + label={label} + color={palette[idx]} + icon={ + customMarkers && Array.isArray(customMarkers) + ? customMarkers[idx] + : customMarkers + } + maskedIcon={maskedMarkers} + isStrokeColor={isStrokeColor} + /> + ))} + </Box> ); } @@ -113,56 +117,33 @@ const Marker = styled(Box, { : getCircleStyles({ isMax, color, isStrokeColor, theme })) })); -const LongTruncate = styled(Typography)(() => ({ - flex: 1, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis' -})); - -const TitlePhantom = styled(LongTruncate)(() => ({ - opacity: 0, - position: 'absolute', - whiteSpace: 'nowrap', - pointerEvents: 'none' -})); - -function Row({ label, isMax, isStrokeColor, color = '#000', icon, maskedIcon }) { - const [showTooltip, setShowTooltip] = useState(false); - const labelRef = useRef(null); - const labelPhantomRef = useRef(null); - - useEffect(() => { - if (!labelPhantomRef?.current || !labelRef?.current) { - return; - } - const labelSizes = labelRef?.current.getBoundingClientRect(); - const labelPhantomSizes = labelPhantomRef?.current.getBoundingClientRect(); - setShowTooltip(labelPhantomSizes.width > labelSizes.width); - }, [setShowTooltip, labelPhantomRef, labelRef]); - +function LegendCategoriesRow({ + label, + isMax, + isStrokeColor, + color = '#000', + icon, + maskedIcon +}) { return ( - <Tooltip title={showTooltip ? label : ''} placement='left'> - <Grid container item alignContent={'center'}> - <Tooltip title={isMax ? 'Most representative' : ''}> - <Marker - className='marker' - mr={1.5} - component='span' - isMax={isMax} - icon={icon} - maskedIcon={maskedIcon} - isStrokeColor={isStrokeColor} - color={color} - /> - </Tooltip> - <LongTruncate ref={labelRef} variant='overlineDelicate'> - {label} - </LongTruncate> - <TitlePhantom ref={labelPhantomRef} variant='overlineDelicate'> - {label} - </TitlePhantom> - </Grid> - </Tooltip> + <Box component='li' sx={{ display: 'flex', alignItems: 'center' }}> + <Tooltip title={isMax ? 'Most representative' : ''}> + <Marker + className='marker' + mr={1.5} + component='span' + isMax={isMax} + icon={icon} + maskedIcon={maskedIcon} + isStrokeColor={isStrokeColor} + color={color} + /> + </Tooltip> + <LegendLayerTitle + title={label} + visible + typographyProps={{ variant: 'overlineDelicate' }} + /> + </Box> ); } diff --git a/packages/react-ui/src/widgets/legend/LegendIcon.js b/packages/react-ui/src/widgets/legend/LegendIcon.js index 7e67aa095..865c36c12 100644 --- a/packages/react-ui/src/widgets/legend/LegendIcon.js +++ b/packages/react-ui/src/widgets/legend/LegendIcon.js @@ -1,30 +1,38 @@ import React from 'react'; -import { Box, Grid } from '@mui/material'; +import { Box } from '@mui/material'; import PropTypes from 'prop-types'; import Typography from '../../components/atoms/Typography'; import { ICON_SIZE_MEDIUM } from '../../theme/themeConstants'; function LegendIcon({ legend }) { const { labels = [], icons = [] } = legend; - - const Icons = labels.map((label, idx) => ( - <Grid key={label} container item alignItems='center'> - <Box mr={1.5} width={ICON_SIZE_MEDIUM} height={ICON_SIZE_MEDIUM}> - <img - src={icons[idx]} - alt={label} - width={ICON_SIZE_MEDIUM} - height={ICON_SIZE_MEDIUM} - /> - </Box> - <Typography variant='overlineDelicate'>{label}</Typography> - </Grid> - )); - return ( - <Grid container direction='column' spacing={1} data-testid='icon-legend'> - {Icons} - </Grid> + <Box + component='ul' + data-testid='icon-legend' + sx={{ m: 0, p: 0, pb: 1, display: 'flex', flexDirection: 'column' }} + > + {labels.map((label, idx) => ( + <Box key={label} component='li' sx={{ display: 'flex', alignItems: 'center' }}> + <Box + sx={{ + mr: 1.5, + width: ICON_SIZE_MEDIUM, + height: ICON_SIZE_MEDIUM, + '& img': { + margin: 'auto', + display: 'block' + } + }} + > + <img src={icons[idx]} alt={label} width='autio' height={ICON_SIZE_MEDIUM} /> + </Box> + <Typography lineHeight='24px' variant='overlineDelicate'> + {label} + </Typography> + </Box> + ))} + </Box> ); } diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js b/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js index 92bb6bec5..dc0b96218 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js @@ -5,9 +5,10 @@ import { useLayoutEffect, useRef, useState } from 'react'; * @param {object} props * @param {string} props.title * @param {boolean} props.visible + * @param {object} props.typographyProps * @returns {React.ReactNode} */ -export default function LegendLayerTitle({ title, visible }) { +export default function LegendLayerTitle({ title, visible, typographyProps }) { const ref = useRef(null); const [isOverflow, setIsOverflow] = useState(false); @@ -28,6 +29,7 @@ export default function LegendLayerTitle({ title, visible }) { component='p' noWrap sx={{ my: 0.25 }} + {...typographyProps} > {title} </Typography> From 1ccb75c72acb13eb406ff4b0c897d8ce277291a0 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 8 Feb 2024 10:36:11 +0100 Subject: [PATCH 27/66] extract list styles --- .../src/widgets/legend/LegendCategories.js | 9 +++----- .../react-ui/src/widgets/legend/LegendIcon.js | 21 ++++-------------- .../new-legend/LegendWidgetUI.styles.js | 22 +++++++++++++++++++ 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/packages/react-ui/src/widgets/legend/LegendCategories.js b/packages/react-ui/src/widgets/legend/LegendCategories.js index 08744d34d..d0d717f85 100644 --- a/packages/react-ui/src/widgets/legend/LegendCategories.js +++ b/packages/react-ui/src/widgets/legend/LegendCategories.js @@ -3,6 +3,7 @@ import { Box, Tooltip, styled } from '@mui/material'; import { getPalette } from '../../utils/palette'; import PropTypes from 'prop-types'; import LegendLayerTitle from '../new-legend/LegendLayerTitle'; +import { styles } from '../new-legend/LegendWidgetUI.styles'; function LegendCategories({ legend }) { const { @@ -16,11 +17,7 @@ function LegendCategories({ legend }) { const palette = getPalette(colors, labels.length); return ( - <Box - component='ul' - data-testid='categories-legend' - sx={{ m: 0, p: 0, pb: 1, display: 'flex', flexDirection: 'column' }} - > + <Box component='ul' data-testid='categories-legend' sx={styles.legendVariableList}> {labels.map((label, idx) => ( <LegendCategoriesRow key={label + idx} @@ -126,7 +123,7 @@ function LegendCategoriesRow({ maskedIcon }) { return ( - <Box component='li' sx={{ display: 'flex', alignItems: 'center' }}> + <Box component='li' sx={styles.legendVariableListItem}> <Tooltip title={isMax ? 'Most representative' : ''}> <Marker className='marker' diff --git a/packages/react-ui/src/widgets/legend/LegendIcon.js b/packages/react-ui/src/widgets/legend/LegendIcon.js index 865c36c12..b22b23d58 100644 --- a/packages/react-ui/src/widgets/legend/LegendIcon.js +++ b/packages/react-ui/src/widgets/legend/LegendIcon.js @@ -3,28 +3,15 @@ import { Box } from '@mui/material'; import PropTypes from 'prop-types'; import Typography from '../../components/atoms/Typography'; import { ICON_SIZE_MEDIUM } from '../../theme/themeConstants'; +import { styles } from '../new-legend/LegendWidgetUI.styles'; function LegendIcon({ legend }) { const { labels = [], icons = [] } = legend; return ( - <Box - component='ul' - data-testid='icon-legend' - sx={{ m: 0, p: 0, pb: 1, display: 'flex', flexDirection: 'column' }} - > + <Box component='ul' data-testid='icon-legend' sx={styles.legendVariableList}> {labels.map((label, idx) => ( - <Box key={label} component='li' sx={{ display: 'flex', alignItems: 'center' }}> - <Box - sx={{ - mr: 1.5, - width: ICON_SIZE_MEDIUM, - height: ICON_SIZE_MEDIUM, - '& img': { - margin: 'auto', - display: 'block' - } - }} - > + <Box key={label} component='li' sx={styles.legendVariableListItem}> + <Box sx={styles.legendIconWrapper}> <img src={icons[idx]} alt={label} width='autio' height={ICON_SIZE_MEDIUM} /> </Box> <Typography lineHeight='24px' variant='overlineDelicate'> diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js index ea0f4ae31..dcaaa1960 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js @@ -1,3 +1,5 @@ +import { ICON_SIZE_MEDIUM } from '../..'; + export const LEGEND_WIDTH = 240; export const styles = { root: { @@ -83,6 +85,26 @@ export const styles = { width: '60px', flexShrink: 0 }, + legendVariableList: { + m: 0, + p: 0, + pb: 1, + display: 'flex', + flexDirection: 'column' + }, + legendVariableListItem: { + display: 'flex', + alignItems: 'center' + }, + legendIconWrapper: { + mr: 1.5, + width: ICON_SIZE_MEDIUM, + height: ICON_SIZE_MEDIUM, + '& img': { + margin: 'auto', + display: 'block' + } + }, 'top-left': { top: 0, left: 0 From 674a06ebc34d7982e72a6da2883298d5b8428965 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 8 Feb 2024 10:42:31 +0100 Subject: [PATCH 28/66] add min max configurable for proportion legend --- packages/react-ui/src/localization/en.js | 4 +++- .../src/widgets/legend/LegendProportion.js | 20 +++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/react-ui/src/localization/en.js b/packages/react-ui/src/localization/en.js index cc097805d..6ab431cea 100644 --- a/packages/react-ui/src/localization/en.js +++ b/packages/react-ui/src/localization/en.js @@ -56,7 +56,9 @@ const locales = { icon: 'Icon based on', strokeColor: 'Stroke color based on', color: 'Color based on' - } + }, + max: 'Max', + min: 'Min' }, range: { clear: 'Clear', diff --git a/packages/react-ui/src/widgets/legend/LegendProportion.js b/packages/react-ui/src/widgets/legend/LegendProportion.js index a02bd80d6..1894cecf7 100644 --- a/packages/react-ui/src/widgets/legend/LegendProportion.js +++ b/packages/react-ui/src/widgets/legend/LegendProportion.js @@ -2,6 +2,8 @@ import React from 'react'; import { Box, Grid, styled } from '@mui/material'; import PropTypes from 'prop-types'; import Typography from '../../components/atoms/Typography'; +import { useIntl } from 'react-intl'; +import useImperativeIntl from '../../hooks/useImperativeIntl'; const sizes = { 0: 12, @@ -35,6 +37,10 @@ const ProportionalGrid = styled(Grid)(({ theme: { spacing } }) => ({ })); function LegendProportion({ legend }) { + const intl = useIntl(); + const intlConfig = useImperativeIntl(intl); + + const showMinMax = legend.showMinMax; const { min, max, error } = calculateRange(legend); const [step1, step2] = !error ? calculateSteps(min, max) : [0, 0]; @@ -62,7 +68,12 @@ function LegendProportion({ legend }) { ) : ( <> <Grid item> - <Typography variant='overline'>Max: {max}</Typography> + <Typography variant='overline'> + {showMinMax + ? intlConfig.formatMessage({ id: 'c4r.widgets.legend.max' }) + : ''}{' '} + {max} + </Typography> </Grid> <Grid item> <Typography variant='overline'>{step2}</Typography> @@ -71,7 +82,12 @@ function LegendProportion({ legend }) { <Typography variant='overline'>{step1}</Typography> </Grid> <Grid item> - <Typography variant='overline'>Min: {min}</Typography> + <Typography variant='overline'> + {showMinMax + ? intlConfig.formatMessage({ id: 'c4r.widgets.legend.min' }) + : ''}{' '} + {min} + </Typography> </Grid> </> )} From d38a42085fc80e4c6ff78cab37a5b085aae91e24 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 8 Feb 2024 10:43:51 +0100 Subject: [PATCH 29/66] add default min max config for legend ramp --- packages/react-ui/src/widgets/legend/LegendProportion.js | 2 +- packages/react-ui/src/widgets/legend/LegendRamp.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react-ui/src/widgets/legend/LegendProportion.js b/packages/react-ui/src/widgets/legend/LegendProportion.js index 1894cecf7..9387edce7 100644 --- a/packages/react-ui/src/widgets/legend/LegendProportion.js +++ b/packages/react-ui/src/widgets/legend/LegendProportion.js @@ -40,7 +40,7 @@ function LegendProportion({ legend }) { const intl = useIntl(); const intlConfig = useImperativeIntl(intl); - const showMinMax = legend.showMinMax; + const showMinMax = legend.showMinMax ?? true; const { min, max, error } = calculateRange(legend); const [step1, step2] = !error ? calculateSteps(min, max) : [0, 0]; diff --git a/packages/react-ui/src/widgets/legend/LegendRamp.js b/packages/react-ui/src/widgets/legend/LegendRamp.js index 11f7f5d14..ec136ce1d 100644 --- a/packages/react-ui/src/widgets/legend/LegendRamp.js +++ b/packages/react-ui/src/widgets/legend/LegendRamp.js @@ -6,7 +6,7 @@ import { getPalette } from '../../utils/palette'; import LegendProportion, { getMinMax } from './LegendProportion'; function LegendRamp({ isContinuous = false, legend }) { - const { labels = [], colors = [] } = legend; + const { labels = [], colors = [], showMinMax = true } = legend; const palette = getPalette( colors, @@ -26,7 +26,7 @@ function LegendRamp({ isContinuous = false, legend }) { let maxLabel = formattedLabels[formattedLabels.length - 1]; let minLabel = formattedLabels[0]; - if (!isContinuous) { + if (!isContinuous && showMinMax) { minLabel = '< ' + minLabel; maxLabel = '≥ ' + maxLabel; } From 17c7aa5a2f2a6fd79f57a8c1b6f47e0719efee88 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 8 Feb 2024 11:47:22 +0100 Subject: [PATCH 30/66] wip mobile menu --- .../src/widgets/new-legend/LegendWidgetUI.js | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js index 40f1fa85b..efdfc3d88 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js @@ -1,5 +1,14 @@ import PropTypes from 'prop-types'; -import { Box, Collapse, IconButton, Paper, Tooltip, Typography } from '@mui/material'; +import { + Box, + Collapse, + Drawer, + IconButton, + Paper, + Tooltip, + Typography, + useMediaQuery +} from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; import LayerIcon from '@mui/icons-material/LayersOutlined'; import { LEGEND_WIDTH, styles } from './LegendWidgetUI.styles'; @@ -47,6 +56,7 @@ function NewLegendWidgetUI({ } = {}) { const intl = useIntl(); const intlConfig = useImperativeIntl(intl); + const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm')); const rootSx = { ...styles[position], @@ -79,8 +89,8 @@ function NewLegendWidgetUI({ </Tooltip> </Box> )} - <Box sx={{ ...styles.legendItemList, width: collapsed ? 0 : undefined }}> - <Collapse unmountOnExit in={!collapsed} timeout={500}> + {isMobile ? ( + <Drawer anchor='bottom' open={!collapsed} onClose={() => onChangeCollapsed(true)}> {layers.map((l) => ( <LegendLayer key={l.id} @@ -95,8 +105,27 @@ function NewLegendWidgetUI({ customLegendTypes={customLegendTypes} /> ))} - </Collapse> - </Box> + </Drawer> + ) : ( + <Box sx={{ ...styles.legendItemList, width: collapsed ? 0 : undefined }}> + <Collapse unmountOnExit in={!collapsed} timeout={500}> + {layers.map((l) => ( + <LegendLayer + key={l.id} + layer={l} + onChangeCollapsed={onChangeLegendRowCollapsed} + onChangeOpacity={onChangeOpacity} + onChangeVisibility={onChangeVisibility} + onChangeSelection={onChangeSelection} + maxZoom={maxZoom} + minZoom={minZoom} + currentZoom={currentZoom} + customLegendTypes={customLegendTypes} + /> + ))} + </Collapse> + </Box> + )} </Paper> ); } From f5cbccb234311dd50f0867d17a59c610bbc5f842 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 8 Feb 2024 11:52:27 +0100 Subject: [PATCH 31/66] update react imports --- packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js | 1 + .../storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js b/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js index dc0b96218..4065d8698 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js @@ -1,3 +1,4 @@ +import React from 'react'; import { Tooltip, Typography } from '@mui/material'; import { useLayoutEffect, useRef, useState } from 'react'; diff --git a/packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js b/packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js index e0ef24d0b..7699be63d 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js @@ -1,4 +1,4 @@ -import React, { useReducer } from 'react'; +import { useReducer, useState } from 'react'; import LegendWidgetUI from '../../../src/widgets/new-legend/LegendWidgetUI'; import { IntlProvider } from 'react-intl'; import { Box } from '@mui/material'; @@ -56,7 +56,7 @@ const Widget = ({ height, ...props }) => ( ); function useLegendState(args) { - const [collapsed, setCollapsed] = React.useState(args.collapsed); + const [collapsed, setCollapsed] = useState(args.collapsed); const [layers, dispatch] = useReducer((state, action) => { switch (action.type) { case 'add': From 53b05b6886e0a2d2a051e2b40f5cab5dadf99a7f Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 8 Feb 2024 12:22:39 +0100 Subject: [PATCH 32/66] fix the weirdest import bug ever --- .../react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js index dcaaa1960..077879bbf 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js @@ -1,4 +1,4 @@ -import { ICON_SIZE_MEDIUM } from '../..'; +import { ICON_SIZE_MEDIUM } from '../../theme/themeConstants'; export const LEGEND_WIDTH = 240; export const styles = { From 746e09a1b67f8c3ff1465cbcbc7d5b6e7cd9332f Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 8 Feb 2024 13:16:20 +0100 Subject: [PATCH 33/66] finish mobile config --- .../react-ui/src/widgets/legend/LegendIcon.js | 10 ++- .../widgets/new-legend/LegendLayerTitle.js | 8 +- .../src/widgets/new-legend/LegendWidgetUI.js | 81 +++++++++++-------- 3 files changed, 56 insertions(+), 43 deletions(-) diff --git a/packages/react-ui/src/widgets/legend/LegendIcon.js b/packages/react-ui/src/widgets/legend/LegendIcon.js index b22b23d58..de3a80efc 100644 --- a/packages/react-ui/src/widgets/legend/LegendIcon.js +++ b/packages/react-ui/src/widgets/legend/LegendIcon.js @@ -1,9 +1,9 @@ import React from 'react'; import { Box } from '@mui/material'; import PropTypes from 'prop-types'; -import Typography from '../../components/atoms/Typography'; import { ICON_SIZE_MEDIUM } from '../../theme/themeConstants'; import { styles } from '../new-legend/LegendWidgetUI.styles'; +import LegendLayerTitle from '../new-legend/LegendLayerTitle'; function LegendIcon({ legend }) { const { labels = [], icons = [] } = legend; @@ -14,9 +14,11 @@ function LegendIcon({ legend }) { <Box sx={styles.legendIconWrapper}> <img src={icons[idx]} alt={label} width='autio' height={ICON_SIZE_MEDIUM} /> </Box> - <Typography lineHeight='24px' variant='overlineDelicate'> - {label} - </Typography> + <LegendLayerTitle + visible + title={label} + typographyProps={{ variant: 'overlineDelicate' }} + /> </Box> ))} </Box> diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js b/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js index 4065d8698..3e03bae54 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js @@ -1,6 +1,6 @@ -import React from 'react'; -import { Tooltip, Typography } from '@mui/material'; -import { useLayoutEffect, useRef, useState } from 'react'; +import React, { useLayoutEffect, useRef, useState } from 'react'; +import { Tooltip } from '@mui/material'; +import { Typography } from '@carto/react-ui'; /** Renders the legend layer title with an optional tooltip if the title is detected to be too long. * @param {object} props @@ -25,7 +25,7 @@ export default function LegendLayerTitle({ title, visible, typographyProps }) { ref={ref} color={visible ? 'textPrimary' : 'textSecondary'} variant='button' - fontWeight={500} + weight='medium' lineHeight='20px' component='p' noWrap diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js index efdfc3d88..c7050d310 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js @@ -6,8 +6,7 @@ import { IconButton, Paper, Tooltip, - Typography, - useMediaQuery + Typography } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; import LayerIcon from '@mui/icons-material/LayersOutlined'; @@ -36,6 +35,7 @@ const EMPTY_ARR = []; * @param {number} [props.maxZoom] - Global maximum zoom level for the map. * @param {number} [props.minZoom] - Global minimum zoom level for the map. * @param {number} [props.currentZoom] - Current zoom level for the map. + * @param {boolean} [props.isMobile] - Whether the widget is displayed on a mobile device. * @returns {React.ReactNode} */ function NewLegendWidgetUI({ @@ -52,11 +52,11 @@ function NewLegendWidgetUI({ position = 'bottom-right', maxZoom = 21, minZoom = 0, - currentZoom + currentZoom, + isMobile } = {}) { const intl = useIntl(); const intlConfig = useImperativeIntl(intl); - const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm')); const rootSx = { ...styles[position], @@ -64,6 +64,24 @@ function NewLegendWidgetUI({ width: collapsed ? undefined : LEGEND_WIDTH }; + const legendToggleHeader = ( + <Box + sx={{ + ...styles.legendToggle, + ...(!collapsed && styles.legendToggleOpen) + }} + > + <Typography variant='caption' sx={{ flexGrow: 1 }}> + {title} + </Typography> + <Tooltip title={intlConfig.formatMessage({ id: 'c4r.widgets.legend.close' })}> + <IconButton size='small' onClick={() => onChangeCollapsed(true)}> + <CloseIcon /> + </IconButton> + </Tooltip> + </Box> + ); + return ( <Paper elevation={3} sx={rootSx}> {collapsed ? ( @@ -72,39 +90,28 @@ function NewLegendWidgetUI({ <LayerIcon /> </IconButton> </Tooltip> - ) : ( - <Box - sx={{ - ...styles.legendToggle, - ...(!collapsed && styles.legendToggleOpen) - }} - > - <Typography variant='caption' sx={{ flexGrow: 1 }}> - {title} - </Typography> - <Tooltip title={intlConfig.formatMessage({ id: 'c4r.widgets.legend.close' })}> - <IconButton size='small' onClick={() => onChangeCollapsed(true)}> - <CloseIcon /> - </IconButton> - </Tooltip> - </Box> + ) : isMobile ? null : ( + legendToggleHeader )} {isMobile ? ( <Drawer anchor='bottom' open={!collapsed} onClose={() => onChangeCollapsed(true)}> - {layers.map((l) => ( - <LegendLayer - key={l.id} - layer={l} - onChangeCollapsed={onChangeLegendRowCollapsed} - onChangeOpacity={onChangeOpacity} - onChangeVisibility={onChangeVisibility} - onChangeSelection={onChangeSelection} - maxZoom={maxZoom} - minZoom={minZoom} - currentZoom={currentZoom} - customLegendTypes={customLegendTypes} - /> - ))} + {legendToggleHeader} + <Box style={styles.legendItemList}> + {layers.map((l) => ( + <LegendLayer + key={l.id} + layer={l} + onChangeCollapsed={onChangeLegendRowCollapsed} + onChangeOpacity={onChangeOpacity} + onChangeVisibility={onChangeVisibility} + onChangeSelection={onChangeSelection} + maxZoom={maxZoom} + minZoom={minZoom} + currentZoom={currentZoom} + customLegendTypes={customLegendTypes} + /> + ))} + </Box> </Drawer> ) : ( <Box sx={{ ...styles.legendItemList, width: collapsed ? 0 : undefined }}> @@ -149,7 +156,11 @@ NewLegendWidgetUI.propTypes = { onChangeSelection: PropTypes.func.isRequired, layerOrder: PropTypes.arrayOf(PropTypes.string), title: PropTypes.string, - position: PropTypes.oneOf(['top-left', 'top-right', 'bottom-left', 'bottom-right']) + position: PropTypes.oneOf(['top-left', 'top-right', 'bottom-left', 'bottom-right']), + maxZoom: PropTypes.number, + minZoom: PropTypes.number, + currentZoom: PropTypes.number, + isMobile: PropTypes.bool }; export default NewLegendWidgetUI; From 16491d3b404b6eac969f62062434849a2bc66abe Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 8 Feb 2024 13:21:59 +0100 Subject: [PATCH 34/66] fix typography import --- packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js b/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js index 3e03bae54..ccd591ff0 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js @@ -1,6 +1,6 @@ import React, { useLayoutEffect, useRef, useState } from 'react'; import { Tooltip } from '@mui/material'; -import { Typography } from '@carto/react-ui'; +import Typography from '../../components/atoms/Typography'; /** Renders the legend layer title with an optional tooltip if the title is detected to be too long. * @param {object} props From f18e63f8b5594f1bcf836786f15fb821c4a0f663 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 8 Feb 2024 16:41:13 +0100 Subject: [PATCH 35/66] refactor legend proportion --- .../widgets/legend/LegendProportion.test.js | 4 +- .../src/widgets/legend/LegendProportion.js | 93 ++++++++++--------- 2 files changed, 52 insertions(+), 45 deletions(-) diff --git a/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js b/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js index 3a2758ed9..ef7aafc7c 100644 --- a/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js +++ b/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js @@ -9,10 +9,10 @@ const DEFAULT_LEGEND = { describe('LegendProportion', () => { test('renders correctly', () => { render(<LegendProportion legend={DEFAULT_LEGEND} />); - expect(screen.queryByText('Max: 200')).toBeInTheDocument(); + expect(screen.queryByText('MAX: 200')).toBeInTheDocument(); expect(screen.queryByText('150')).toBeInTheDocument(); expect(screen.queryByText('50')).toBeInTheDocument(); - expect(screen.queryByText('Min: 0')).toBeInTheDocument(); + expect(screen.queryByText('MIN: 0')).toBeInTheDocument(); }); test('renders error if neither labels is defined', () => { render(<LegendProportion legend={{}} />); diff --git a/packages/react-ui/src/widgets/legend/LegendProportion.js b/packages/react-ui/src/widgets/legend/LegendProportion.js index 9387edce7..2369cdc34 100644 --- a/packages/react-ui/src/widgets/legend/LegendProportion.js +++ b/packages/react-ui/src/widgets/legend/LegendProportion.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Box, Grid, styled } from '@mui/material'; +import { Box, styled } from '@mui/material'; import PropTypes from 'prop-types'; import Typography from '../../components/atoms/Typography'; import { useIntl } from 'react-intl'; @@ -30,10 +30,30 @@ const Circle = styled(Box, { }; }); -const ProportionalGrid = styled(Grid)(({ theme: { spacing } }) => ({ +const CircleGrid = styled(Box)(({ theme: { spacing } }) => ({ + display: 'flex', justifyContent: 'flex-end', - marginBottom: spacing(0.5), - position: 'relative' + flexShrink: 0, + position: 'relative', + width: spacing(sizes[0]), + height: spacing(sizes[0]) +})); + +const LegendProportionWrapper = styled(Box)(({ theme: { spacing } }) => ({ + display: 'flex', + gap: spacing(1), + alignItems: 'stretch', + justifyContent: 'stretch', + padding: spacing(2, 0) +})); + +const LabelList = styled(Box)(() => ({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-around', + gap: 1, + flexGrow: 1, + flexShrink: 1 })); function LegendProportion({ legend }) { @@ -45,54 +65,41 @@ function LegendProportion({ legend }) { const [step1, step2] = !error ? calculateSteps(min, max) : [0, 0]; return ( - <Grid container item direction='row' spacing={2} data-testid='proportion-legend'> - <ProportionalGrid container item xs={6}> - {[...Array(4)].map((circle, index) => ( - <Circle key={index} index={index} component='span'></Circle> + <LegendProportionWrapper data-testid='proportion-legend'> + <CircleGrid> + {[...Array(4)].map((_, index) => ( + <Circle key={index} index={index} component='span' /> ))} - </ProportionalGrid> - <Grid - container - item - direction='column' - justifyContent='space-between' - xs={6} - spacing={1} - > + </CircleGrid> + <LabelList> {error ? ( - <Grid item maxWidth={240}> + <Box maxWidth={240}> <Typography variant='overline'> You need to specify valid numbers for the labels property </Typography> - </Grid> + </Box> ) : ( <> - <Grid item> - <Typography variant='overline'> - {showMinMax - ? intlConfig.formatMessage({ id: 'c4r.widgets.legend.max' }) - : ''}{' '} - {max} - </Typography> - </Grid> - <Grid item> - <Typography variant='overline'>{step2}</Typography> - </Grid> - <Grid item> - <Typography variant='overline'>{step1}</Typography> - </Grid> - <Grid item> - <Typography variant='overline'> - {showMinMax - ? intlConfig.formatMessage({ id: 'c4r.widgets.legend.min' }) - : ''}{' '} - {min} - </Typography> - </Grid> + <Typography variant='overline' color='textSecondary'> + {showMinMax + ? `${intlConfig.formatMessage({ id: 'c4r.widgets.legend.max' })}: ${max}` + : max} + </Typography> + <Typography variant='overline' color='textSecondary'> + {step2} + </Typography> + <Typography variant='overline' color='textSecondary'> + {step1} + </Typography> + <Typography variant='overline' color='textSecondary'> + {showMinMax + ? `${intlConfig.formatMessage({ id: 'c4r.widgets.legend.min' })}: ${min}` + : min} + </Typography> </> )} - </Grid> - </Grid> + </LabelList> + </LegendProportionWrapper> ); } From 885114b66a4fe8c49f7b66ab9cf0d08e05b7bd7d Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 8 Feb 2024 16:41:27 +0100 Subject: [PATCH 36/66] use typography atom component --- .../src/widgets/new-legend/LegendLayerVariable.js | 14 +++++++++++--- .../storybook/stories/widgetsUI/legendFixtures.js | 2 ++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js index 31683fd52..dd3e95749 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js @@ -1,4 +1,4 @@ -import { Box, ListItemText, MenuItem, Select, Typography } from '@mui/material'; +import { Box, ListItemText, MenuItem, Select } from '@mui/material'; import LegendCategories from '../legend/LegendCategories'; import LegendIcon from '../legend/LegendIcon'; import LegendRamp from '../legend/LegendRamp'; @@ -6,6 +6,7 @@ import LegendProportion from '../legend/LegendProportion'; import { LEGEND_TYPES } from '../legend/LegendWidgetUI'; import { useIntl } from 'react-intl'; import useImperativeIntl from '../../hooks/useImperativeIntl'; +import Typography from '../../components/atoms/Typography'; const legendTypeMap = { [LEGEND_TYPES.CATEGORY]: LegendCategories, @@ -78,7 +79,12 @@ export default function LegendLayerVariable({ <Box data-testid='legend-layer-variable' px={2}> {legend.attr ? ( <Box pb={1}> - <Typography variant='overlineDelicate'> + <Typography + gutterBottom + variant='overlineDelicate' + color='textSecondary' + component='p' + > {intlConfig.formatMessage({ id: getLegendSubtitle(legend) })} </Typography> <Typography variant='caption' component='p'> @@ -88,7 +94,9 @@ export default function LegendLayerVariable({ ) : null} {legend.select ? ( <Box pb={1}> - <Typography variant='caption'>{legend.select.label}</Typography> + <Typography variant='caption' weight='medium' component='p'> + {legend.select.label} + </Typography> <Select value={legend.select.value} renderValue={(value) => diff --git a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js index 2b0a8bd5f..e27db194e 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js +++ b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js @@ -47,6 +47,7 @@ export const fixtures = [ switchable: true, legend: [ { + attr: 'category', collapsed: false, type: LEGEND_TYPES.ICON, icons: [ @@ -60,6 +61,7 @@ export const fixtures = [ labels: ['Available land'] }, { + attr: 'population', collapsed: false, type: LEGEND_TYPES.PROPORTION, labels: [1, 1000] From 45fa551632fca0938e892c04f2ca0fe36176ec9c Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 8 Feb 2024 16:52:12 +0100 Subject: [PATCH 37/66] update jsdocs --- packages/react-ui/src/widgets/legend/LegendCategories.js | 5 +++++ packages/react-ui/src/widgets/legend/LegendIcon.js | 5 +++++ packages/react-ui/src/widgets/legend/LegendProportion.js | 5 +++++ packages/react-ui/src/widgets/legend/LegendRamp.js | 6 ++++++ packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts | 7 ++++--- .../react-ui/src/widgets/new-legend/LegendLayerVariable.js | 2 +- 6 files changed, 26 insertions(+), 4 deletions(-) diff --git a/packages/react-ui/src/widgets/legend/LegendCategories.js b/packages/react-ui/src/widgets/legend/LegendCategories.js index d0d717f85..8d5851339 100644 --- a/packages/react-ui/src/widgets/legend/LegendCategories.js +++ b/packages/react-ui/src/widgets/legend/LegendCategories.js @@ -5,6 +5,11 @@ import PropTypes from 'prop-types'; import LegendLayerTitle from '../new-legend/LegendLayerTitle'; import { styles } from '../new-legend/LegendWidgetUI.styles'; +/** + * @param {object} props + * @param {import('../legend/LegendWidgetUI').LegendLayerVariableBase & import('../legend/LegendWidgetUI').LegendCategories} props.legend - legend variable data. + * @returns {React.ReactNode} + */ function LegendCategories({ legend }) { const { labels = [], diff --git a/packages/react-ui/src/widgets/legend/LegendIcon.js b/packages/react-ui/src/widgets/legend/LegendIcon.js index de3a80efc..da2bfdd26 100644 --- a/packages/react-ui/src/widgets/legend/LegendIcon.js +++ b/packages/react-ui/src/widgets/legend/LegendIcon.js @@ -5,6 +5,11 @@ import { ICON_SIZE_MEDIUM } from '../../theme/themeConstants'; import { styles } from '../new-legend/LegendWidgetUI.styles'; import LegendLayerTitle from '../new-legend/LegendLayerTitle'; +/** + * @param {object} props + * @param {import('../legend/LegendWidgetUI').LegendLayerVariableBase & import('../legend/LegendWidgetUI').LegendIcons} props.legend - legend variable data. + * @returns {React.ReactNode} + */ function LegendIcon({ legend }) { const { labels = [], icons = [] } = legend; return ( diff --git a/packages/react-ui/src/widgets/legend/LegendProportion.js b/packages/react-ui/src/widgets/legend/LegendProportion.js index 2369cdc34..273f00f4a 100644 --- a/packages/react-ui/src/widgets/legend/LegendProportion.js +++ b/packages/react-ui/src/widgets/legend/LegendProportion.js @@ -56,6 +56,11 @@ const LabelList = styled(Box)(() => ({ flexShrink: 1 })); +/** + * @param {object} props + * @param {import('../legend/LegendWidgetUI').LegendLayerVariableBase & import('../legend/LegendWidgetUI').LegendProportion} props.legend - legend variable data. + * @returns {React.ReactNode} + */ function LegendProportion({ legend }) { const intl = useIntl(); const intlConfig = useImperativeIntl(intl); diff --git a/packages/react-ui/src/widgets/legend/LegendRamp.js b/packages/react-ui/src/widgets/legend/LegendRamp.js index ec136ce1d..069d9fafe 100644 --- a/packages/react-ui/src/widgets/legend/LegendRamp.js +++ b/packages/react-ui/src/widgets/legend/LegendRamp.js @@ -5,6 +5,12 @@ import Typography from '../../components/atoms/Typography'; import { getPalette } from '../../utils/palette'; import LegendProportion, { getMinMax } from './LegendProportion'; +/** + * @param {object} props + * @param {import('../legend/LegendWidgetUI').LegendLayerVariableBase & import('../legend/LegendWidgetUI').LegendRamp} props.legend - legend variable data. + * @param {boolean} [props.isContinuous] - If the legend is continuous. + * @returns {React.ReactNode} + */ function LegendRamp({ isContinuous = false, legend }) { const { labels = [], colors = [], showMinMax = true } = legend; diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts index 58551bd22..06bfea0b2 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts @@ -23,11 +23,12 @@ export type LegendLayerData = { legend?: LegendLayerVariableData | LegendLayerVariableData[]; }; -export type LegendLayerVariableData = { +export type LegendLayerVariableBase = { type: LEGEND_TYPES; select: LegendSelectConfig - attr?: React.ReactNode; // subtitle to show below the legend item toggle when expanded -} & LegendType; + attr?: string; // subtitle to show below the legend item toggle when expanded +} +export type LegendLayerVariableData = LegendLayerVariableBase & LegendType; type LegendType = LegendBins | LegendRamp | LegendIcons | LegendCategories | LegendProportion; diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js index dd3e95749..3593010a5 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js @@ -18,7 +18,7 @@ const legendTypeMap = { /** * @param {object} props - * @param {import('../legend/LegendWidgetUI').LegendLayerVariableData} props.legend - legend variable data. + * @param {import('../legend/LegendWidgetUI').LegendLayerVariableBase} props.legend - legend variable data. * @returns {React.ReactNode} */ function LegendUnknown({ legend }) { From 04aa1c86e48bbd5c623ce131565ee2784d23ab0e Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 8 Feb 2024 17:19:09 +0100 Subject: [PATCH 38/66] update all legends finished, and tests passing --- .../widgets/legend/LegendProportion.test.js | 24 ++++++++++-- .../widgets/legend/LegendRamp.test.js | 7 ++++ .../src/widgets/legend/LegendProportion.js | 5 +++ .../react-ui/src/widgets/legend/LegendRamp.js | 38 +++++++++++-------- .../legend/LegendProportion.stories.js | 7 +++- 5 files changed, 62 insertions(+), 19 deletions(-) diff --git a/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js b/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js index ef7aafc7c..f1a4c8201 100644 --- a/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js +++ b/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js @@ -1,6 +1,7 @@ import React from 'react'; import { render, screen } from '../../widgets/utils/testUtils'; import LegendProportion from '../../../src/widgets/legend/LegendProportion'; +import { IntlProvider } from 'react-intl'; const DEFAULT_LEGEND = { labels: ['0', '200'] @@ -8,11 +9,28 @@ const DEFAULT_LEGEND = { describe('LegendProportion', () => { test('renders correctly', () => { - render(<LegendProportion legend={DEFAULT_LEGEND} />); - expect(screen.queryByText('MAX: 200')).toBeInTheDocument(); + render( + <IntlProvider locale='en'> + <LegendProportion legend={DEFAULT_LEGEND} /> + </IntlProvider> + ); + expect(screen.queryByText('Max: 200')).toBeInTheDocument(); expect(screen.queryByText('150')).toBeInTheDocument(); expect(screen.queryByText('50')).toBeInTheDocument(); - expect(screen.queryByText('MIN: 0')).toBeInTheDocument(); + expect(screen.queryByText('Min: 0')).toBeInTheDocument(); + }); + test('renders correctly without min and max', () => { + render( + <IntlProvider locale='en'> + <LegendProportion legend={{ ...DEFAULT_LEGEND, showMinMax: false }} /> + </IntlProvider> + ); + expect(screen.queryByText('Max')).not.toBeInTheDocument(); + expect(screen.queryByText('Min')).not.toBeInTheDocument(); + expect(screen.queryByText('200')).toBeInTheDocument(); + expect(screen.queryByText('150')).toBeInTheDocument(); + expect(screen.queryByText('50')).toBeInTheDocument(); + expect(screen.queryByText('0')).toBeInTheDocument(); }); test('renders error if neither labels is defined', () => { render(<LegendProportion legend={{}} />); diff --git a/packages/react-ui/__tests__/widgets/legend/LegendRamp.test.js b/packages/react-ui/__tests__/widgets/legend/LegendRamp.test.js index 8edfd8d8d..11d3d524c 100644 --- a/packages/react-ui/__tests__/widgets/legend/LegendRamp.test.js +++ b/packages/react-ui/__tests__/widgets/legend/LegendRamp.test.js @@ -41,6 +41,13 @@ describe('LegendRamp', () => { expect(backgroundColor).toBe(hexToRgb(color)); }); }); + test('renders correctly without min and max', () => { + render(<LegendRamp legend={{ ...DEFAULT_LEGEND, showMinMax: false }} />); + expect(screen.queryByText('< 0')).not.toBeInTheDocument(); + expect(screen.queryByText('≥ 200')).not.toBeInTheDocument(); + expect(screen.queryByText('0')).toBeInTheDocument(); + expect(screen.queryByText('200')).toBeInTheDocument(); + }); test('renders formatted labels correctly', () => { render(<LegendRamp legend={DEFAULT_LEGEND_WITH_FORMATTED_LABELS} />); expect(screen.queryByText('< 0 km')).toBeInTheDocument(); diff --git a/packages/react-ui/src/widgets/legend/LegendProportion.js b/packages/react-ui/src/widgets/legend/LegendProportion.js index 273f00f4a..01c800924 100644 --- a/packages/react-ui/src/widgets/legend/LegendProportion.js +++ b/packages/react-ui/src/widgets/legend/LegendProportion.js @@ -69,6 +69,11 @@ function LegendProportion({ legend }) { const { min, max, error } = calculateRange(legend); const [step1, step2] = !error ? calculateSteps(min, max) : [0, 0]; + console.log({ + error, + domain: [max, step2, step1, min] + }); + return ( <LegendProportionWrapper data-testid='proportion-legend'> <CircleGrid> diff --git a/packages/react-ui/src/widgets/legend/LegendRamp.js b/packages/react-ui/src/widgets/legend/LegendRamp.js index 069d9fafe..1008fcf2a 100644 --- a/packages/react-ui/src/widgets/legend/LegendRamp.js +++ b/packages/react-ui/src/widgets/legend/LegendRamp.js @@ -1,4 +1,4 @@ -import { Grid, Tooltip, styled } from '@mui/material'; +import { Box, Tooltip, styled } from '@mui/material'; import PropTypes from 'prop-types'; import React from 'react'; import Typography from '../../components/atoms/Typography'; @@ -38,18 +38,18 @@ function LegendRamp({ isContinuous = false, legend }) { } return ( - <Grid container item direction='column' spacing={1} data-testid='ramp-legend'> + <Box sx={{ py: 2 }} data-testid='ramp-legend'> {error ? ( - <Grid item xs maxWidth={240}> + <Box maxWidth={240}> <Typography variant='overline'> You need to specify valid numbers for the labels property </Typography> - </Grid> + </Box> ) : ( <> - <Grid container item> + <Box sx={{ display: 'flex', pb: 1 }}> {isContinuous ? ( - <StepsContinuous data-testid='step-continuous' item xs palette={palette} /> + <StepsContinuous data-testid='step-continuous' palette={palette} /> ) : ( <StepsDiscontinuous labels={formattedLabels} @@ -58,14 +58,18 @@ function LegendRamp({ isContinuous = false, legend }) { min={minLabel} /> )} - </Grid> - <Grid container item justifyContent='space-between'> - <Typography variant='overline'>{minLabel}</Typography> - <Typography variant='overline'>{maxLabel}</Typography> - </Grid> + </Box> + <Box sx={{ display: 'flex', justifyContent: 'space-between' }}> + <Typography variant='overlineDelicate' color='textSecondary'> + {maxLabel} + </Typography> + <Typography variant='overlineDelicate' color='textSecondary'> + {minLabel} + </Typography> + </Box> </> )} - </Grid> + </Box> ); } @@ -92,17 +96,21 @@ LegendRamp.propTypes = { export default LegendRamp; -const StepsContinuous = styled(Grid, { +const StepsContinuous = styled(Box, { shouldForwardProp: (prop) => prop !== 'palette' })(({ palette, theme }) => ({ + display: 'block', + flexGrow: 1, height: theme.spacing(1), borderRadius: theme.spacing(0.5), background: `linear-gradient(to right, ${palette.join()})` })); -const StepGrid = styled(Grid, { +const StepGrid = styled(Box, { shouldForwardProp: (prop) => prop !== 'color' })(({ color, theme }) => ({ + display: 'block', + flexGrow: 1, height: theme.spacing(1), backgroundColor: color, '&:first-of-type': { @@ -128,7 +136,7 @@ function StepsDiscontinuous({ labels = [], palette = [], max, min }) { return ( <Tooltip key={idx} title={title}> - <StepGrid data-testid='step-discontinuous' item xs color={palette[idx]} /> + <StepGrid data-testid='step-discontinuous' color={palette[idx]} /> </Tooltip> ); })} diff --git a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendProportion.stories.js b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendProportion.stories.js index 65d683f35..57057bab9 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendProportion.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendProportion.stories.js @@ -1,5 +1,6 @@ import React from 'react'; import LegendProportion from '../../../../src/widgets/legend/LegendProportion'; +import { IntlProvider } from 'react-intl'; const DEFAULT_LEGEND = { legend: { @@ -25,7 +26,11 @@ const options = { export default options; const Template = (args) => { - return <LegendProportion {...args} />; + return ( + <IntlProvider locale='en'> + <LegendProportion {...args} /> + </IntlProvider> + ); }; export const Default = Template.bind({}); From b2f357aeea91a9be82993aefd9b00c11cc26fa3e Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 8 Feb 2024 17:19:49 +0100 Subject: [PATCH 39/66] remove console log --- packages/react-ui/src/widgets/legend/LegendProportion.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/react-ui/src/widgets/legend/LegendProportion.js b/packages/react-ui/src/widgets/legend/LegendProportion.js index 01c800924..273f00f4a 100644 --- a/packages/react-ui/src/widgets/legend/LegendProportion.js +++ b/packages/react-ui/src/widgets/legend/LegendProportion.js @@ -69,11 +69,6 @@ function LegendProportion({ legend }) { const { min, max, error } = calculateRange(legend); const [step1, step2] = !error ? calculateSteps(min, max) : [0, 0]; - console.log({ - error, - domain: [max, step2, step1, min] - }); - return ( <LegendProportionWrapper data-testid='proportion-legend'> <CircleGrid> From a29a70223e24a8389b50a099ac17e3e62d3e305d Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Fri, 9 Feb 2024 17:46:08 +0100 Subject: [PATCH 40/66] everything done on widget, storybooks and tests --- .../__tests__/widgets/LegendWidgetUI.test.js | 156 +++++----- .../widgets/legend/LegendCategories.test.js | 2 +- .../widgets/legend/LegendIcon.test.js | 2 +- .../widgets/legend/LegendProportion.test.js | 2 +- .../widgets/legend/LegendRamp.test.js | 2 +- .../react-ui/src/assets/icons/OpacityIcon.js | 1 + packages/react-ui/src/index.d.ts | 13 +- packages/react-ui/src/index.js | 11 +- packages/react-ui/src/localization/en.js | 2 +- packages/react-ui/src/types.d.ts | 45 +-- .../src/widgets/legend/LegendWidgetUI.js | 8 +- .../src/widgets/new-legend/LegendLayer.js | 10 +- .../widgets/new-legend/LegendLayerVariable.js | 30 +- .../new-legend/LegendOpacityControl.js | 1 + .../LegendWidgetUI.d.ts | 38 ++- .../src/widgets/new-legend/LegendWidgetUI.js | 174 ++++++----- .../legend-types}/LegendCategories.js | 8 +- .../legend-types}/LegendIcon.js | 8 +- .../legend-types}/LegendProportion.js | 6 +- .../legend-types}/LegendRamp.js | 6 +- .../new-legend/legend-types/LegendTypes.js | 8 + .../widgetsUI/LegendWidgetUI.stories.js | 286 ------------------ .../widgetsUI/NewLegendWidgetUI.stories.js | 253 +++++++++++++++- .../legend/LegendCategories.stories.js | 2 +- .../widgetsUI/legend/LegendIcon.stories.js | 2 +- .../legend/LegendProportion.stories.js | 2 +- .../widgetsUI/legend/LegendRamp.stories.js | 2 +- .../stories/widgetsUI/legendFixtures.js | 2 +- .../react-widgets/src/widgets/LegendWidget.js | 50 ++- 29 files changed, 573 insertions(+), 559 deletions(-) rename packages/react-ui/src/widgets/{legend => new-legend}/LegendWidgetUI.d.ts (56%) rename packages/react-ui/src/widgets/{legend => new-legend/legend-types}/LegendCategories.js (91%) rename packages/react-ui/src/widgets/{legend => new-legend/legend-types}/LegendIcon.js (75%) rename packages/react-ui/src/widgets/{legend => new-legend/legend-types}/LegendProportion.js (92%) rename packages/react-ui/src/widgets/{legend => new-legend/legend-types}/LegendRamp.js (93%) create mode 100644 packages/react-ui/src/widgets/new-legend/legend-types/LegendTypes.js delete mode 100644 packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js diff --git a/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js b/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js index ca2a0e7ec..f091b0520 100644 --- a/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js +++ b/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js @@ -1,38 +1,25 @@ import React from 'react'; -import Typography from '../../src/components/atoms/Typography'; -import LegendWidgetUI from '../../src/widgets/legend/LegendWidgetUI'; +import LegendWidgetUI from '../../src/widgets/new-legend/LegendWidgetUI'; import { fireEvent, render, screen } from '../widgets/utils/testUtils'; -const CUSTOM_CHILDREN = <Typography>Legend custom</Typography>; - const MY_CUSTOM_LEGEND_KEY = 'my-custom-legend'; -const LAYER_OPTIONS = { - PALETTE_SELECTOR: 'PALETTE_SELECTOR' -}; - -const LAYER_OPTIONS_COMPONENTS = { - [LAYER_OPTIONS.PALETTE_SELECTOR]: PaletteSelector -}; - -function PaletteSelector() { - return <p>PaletteSelector</p>; -} - describe('LegendWidgetUI', () => { const DATA = [ { + // 0 id: 'category', title: 'Category Layer', visible: true, + helperText: 'lorem', legend: { type: 'category', - note: 'lorem', colors: ['#000', '#00F', '#0F0'], labels: ['Category 1', 'Category 2', 'Category 3'] } }, { + // 1 id: 'icon', title: 'Icon Layer', visible: true, @@ -47,6 +34,7 @@ describe('LegendWidgetUI', () => { } }, { + // 2 id: 'bins', title: 'Ramp Layer', visible: true, @@ -57,6 +45,7 @@ describe('LegendWidgetUI', () => { } }, { + // 3 id: 'continuous', title: 'Ramp Layer', visible: true, @@ -67,6 +56,7 @@ describe('LegendWidgetUI', () => { } }, { + // 4 id: 'proportion', title: 'Proportion Layer', visible: true, @@ -76,14 +66,7 @@ describe('LegendWidgetUI', () => { } }, { - id: 'custom', - title: 'Single Layer', - visible: true, - legend: { - children: CUSTOM_CHILDREN - } - }, - { + // 5 id: 'custom_key', title: 'Single Layer', visible: true, @@ -95,32 +78,19 @@ describe('LegendWidgetUI', () => { colors: ['#000', '#00F', '#0F0'], labels: ['Category 1', 'Category 2', 'Category 3'] } - }, - { - id: 'custom_children', - title: 'Single Layer', - visible: true, - showOpacityControl: true, - opacity: 0.6, - legend: { - children: CUSTOM_CHILDREN - } - }, - { - id: 'palette', - title: 'Store types', - visible: true, - options: [LAYER_OPTIONS.PALETTE_SELECTOR], - legend: { - children: CUSTOM_CHILDREN - } } ]; + const Widget = (props) => <LegendWidgetUI {...props} />; test('single legend', () => { render(<Widget layers={[DATA[0]]}></Widget>); + // expanded legend toggle expect(screen.queryByText('Layers')).not.toBeInTheDocument(); + // collapsed legend toggle + expect(screen.queryByLabelText('Layers')).not.toBeInTheDocument(); + // layer title + expect(screen.queryByTestId('categories-legend')).toBeInTheDocument(); }); test('multiple legends', () => { @@ -131,7 +101,10 @@ describe('LegendWidgetUI', () => { test('multiple legends with collapsed as true', () => { render(<Widget layers={DATA} collapsed={true}></Widget>); - expect(screen.queryByText('Layers')).toBeInTheDocument(); + // expanded legend toggle + expect(screen.queryByText('Layers')).not.toBeInTheDocument(); + // collapsed legend toggle + expect(screen.queryByLabelText('Layers')).toBeInTheDocument(); expect(screen.queryByTestId('categories-legend')).not.toBeInTheDocument(); }); @@ -160,11 +133,6 @@ describe('LegendWidgetUI', () => { expect(screen.getByTestId('proportion-legend')).toBeInTheDocument(); }); - test('Custom legend', () => { - render(<Widget layers={[DATA[5]]}></Widget>); - expect(screen.getByText('Legend custom')).toBeInTheDocument(); - }); - test('Empty legend', () => { const EMPTY_LAYER = { id: 'empty', title: 'Empty Layer', legend: {} }; render(<Widget layers={[EMPTY_LAYER]}></Widget>); @@ -184,42 +152,52 @@ describe('LegendWidgetUI', () => { test('with custom legend types', () => { const MyCustomLegendComponent = jest.fn(); MyCustomLegendComponent.mockReturnValue(<p>Test</p>); + render( <Widget - layers={[DATA[6]]} + layers={[DATA[5]]} customLegendTypes={{ [MY_CUSTOM_LEGEND_KEY]: MyCustomLegendComponent }} ></Widget> ); + expect(MyCustomLegendComponent).toHaveBeenCalled(); expect(MyCustomLegendComponent).toHaveBeenCalledWith( - { layer: DATA[6], legend: DATA[6].legend }, + { layer: DATA[5], legend: DATA[5].legend }, {} ); expect(screen.getByText('Test')).toBeInTheDocument(); }); test('legend with opacity control', async () => { - const legendConfig = DATA[7]; + const legendConfig = { + id: 'test-opacity-control', + title: 'Test opacity control', + visible: true, + showOpacityControl: true, + opacity: 0.8 + }; const onChangeOpacity = jest.fn(); const container = render( <Widget layers={[legendConfig]} onChangeOpacity={onChangeOpacity}></Widget> ); - const layerOptionsBtn = await screen.findByLabelText('Layer options'); - expect(layerOptionsBtn).toBeInTheDocument(); - layerOptionsBtn.click(); - expect(screen.getByText('Opacity')).toBeInTheDocument(); + + const toggleButton = screen.getByRole('button', { name: 'Opacity' }); + expect(toggleButton).toBeInTheDocument(); + toggleButton.click(); const opacitySelectorInput = container.getByTestId('opacity-slider'); - expect(opacitySelectorInput.value).toBe('' + legendConfig.opacity * 100); + expect(opacitySelectorInput).toBeInTheDocument(); + + expect(opacitySelectorInput.value).toBe(String(legendConfig.opacity * 100)); - fireEvent.change(opacitySelectorInput, { target: { value: '50' } }); + fireEvent.change(opacitySelectorInput, { target: { value: 50 } }); expect(onChangeOpacity).toHaveBeenCalledTimes(1); expect(onChangeOpacity).toHaveBeenCalledWith({ id: legendConfig.id, opacity: 0.5 }); }); test('should manage legend collapsed state correctly', () => { - let legendConfig = { ...DATA[7], legend: { ...DATA[7].legend, collapsed: true } }; + let legendConfig = { ...DATA[0], collapsed: true }; const onChangeLegendRowCollapsed = jest.fn(); const { rerender } = render( @@ -229,11 +207,11 @@ describe('LegendWidgetUI', () => { ></Widget> ); - expect(screen.queryByText('Legend custom')).not.toBeInTheDocument(); + expect(screen.queryByTestId('legend-layer-variable-list')).not.toBeInTheDocument(); - const layerOptionsBtn = screen.getByText('Single Layer'); - expect(layerOptionsBtn).toBeInTheDocument(); - layerOptionsBtn.click(); + const toggleButton = screen.getByRole('button', { name: 'Expand layer' }); + expect(toggleButton).toBeInTheDocument(); + toggleButton.click(); expect(onChangeLegendRowCollapsed).toHaveBeenCalledTimes(1); expect(onChangeLegendRowCollapsed).toHaveBeenCalledWith({ @@ -241,7 +219,8 @@ describe('LegendWidgetUI', () => { collapsed: false }); - legendConfig = { ...DATA[7], legend: { ...DATA[7].legend, collapsed: false } }; + legendConfig = { ...DATA[0], collapsed: false }; + rerender( <Widget layers={[legendConfig]} @@ -249,28 +228,35 @@ describe('LegendWidgetUI', () => { ></Widget> ); - expect(screen.getByText('Legend custom')).toBeInTheDocument(); + expect(screen.getByTestId('legend-layer-variable-list')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Collapse layer' })).toBeInTheDocument(); }); - test('with custom layer options', async () => { - const layer = DATA[8]; - render( - <Widget layers={[layer]} customLayerOptions={LAYER_OPTIONS_COMPONENTS}></Widget> - ); - const layerOptionsBtn = await screen.findByLabelText('Layer options'); - expect(layerOptionsBtn).toBeInTheDocument(); - layerOptionsBtn.click(); - expect(screen.getByText('PaletteSelector')).toBeInTheDocument(); - }); + test('helper text', () => { + render(<Widget layers={[{ ...DATA[0], helperText: 'helperText' }]}></Widget>); - test('with custom layer options - unknown option', async () => { - const layer = { ...DATA[8], options: ['unknown'] }; - render( - <Widget layers={[layer]} customLayerOptions={LAYER_OPTIONS_COMPONENTS}></Widget> - ); - const layerOptionsBtn = await screen.findByLabelText('Layer options'); - expect(layerOptionsBtn).toBeInTheDocument(); - layerOptionsBtn.click(); - expect(screen.getByText('Unknown layer option')).toBeInTheDocument(); + expect(screen.getByText('helperText')).toBeInTheDocument(); }); + + // test('with custom layer options', async () => { + // const layer = DATA[8]; + // render( + // <Widget layers={[layer]} customLayerOptions={LAYER_OPTIONS_COMPONENTS}></Widget> + // ); + // const layerOptionsBtn = await screen.findByLabelText('Layer options'); + // expect(layerOptionsBtn).toBeInTheDocument(); + // layerOptionsBtn.click(); + // expect(screen.getByText('PaletteSelector')).toBeInTheDocument(); + // }); + + // test('with custom layer options - unknown option', async () => { + // const layer = { ...DATA[8], options: ['unknown'] }; + // render( + // <Widget layers={[layer]} customLayerOptions={LAYER_OPTIONS_COMPONENTS}></Widget> + // ); + // const layerOptionsBtn = await screen.findByLabelText('Layer options'); + // expect(layerOptionsBtn).toBeInTheDocument(); + // layerOptionsBtn.click(); + // expect(screen.getByText('Unknown layer option')).toBeInTheDocument(); + // }); }); diff --git a/packages/react-ui/__tests__/widgets/legend/LegendCategories.test.js b/packages/react-ui/__tests__/widgets/legend/LegendCategories.test.js index 914a035b9..26f21cef2 100644 --- a/packages/react-ui/__tests__/widgets/legend/LegendCategories.test.js +++ b/packages/react-ui/__tests__/widgets/legend/LegendCategories.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { render, screen } from '../utils/testUtils'; -import LegendCategories from '../../../src/widgets/legend/LegendCategories'; +import LegendCategories from '../../../src/widgets/new-legend/legend-types/LegendCategories'; import { getPalette } from '../../../src/utils/palette'; import { hexToRgb } from '@mui/material'; diff --git a/packages/react-ui/__tests__/widgets/legend/LegendIcon.test.js b/packages/react-ui/__tests__/widgets/legend/LegendIcon.test.js index 69605b83c..54d97aaa4 100644 --- a/packages/react-ui/__tests__/widgets/legend/LegendIcon.test.js +++ b/packages/react-ui/__tests__/widgets/legend/LegendIcon.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { render, screen } from '../../widgets/utils/testUtils'; -import LegendIcon from '../../../src/widgets/legend/LegendIcon'; +import LegendIcon from '../../../src/widgets/new-legend/legend-types/LegendIcon'; const ICON = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICAgIDxkZWZzPgogICAgICAgIDxmaWx0ZXIgaWQ9InFsbTY3aXI1MWEiPgogICAgICAgICAgICA8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlR3JhcGhpYyIgdmFsdWVzPSIwIDAgMCAwIDAuMTcyNTQ5IDAgMCAwIDAgMC4xODgyMzUgMCAwIDAgMCAwLjE5NjA3OCAwIDAgMCAxLjAwMDAwMCAwIi8+CiAgICAgICAgPC9maWx0ZXI+CiAgICA8L2RlZnM+CiAgICA8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnPgogICAgICAgICAgICA8ZyBmaWx0ZXI9InVybCgjcWxtNjdpcjUxYSkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yNDIgLTQ2NCkgdHJhbnNsYXRlKDIyMiA0NDgpIj4KICAgICAgICAgICAgICAgIDxnIGZpbGw9IiMyQzMwMzIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIwIDE2KSI+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTEyIDBjNi42MjcgMCAxMiA1LjM3MyAxMiAxMnMtNS4zNzMgMTItMTIgMTJTMCAxOC42MjcgMCAxMiA1LjM3MyAwIDEyIDB6IiBvcGFjaXR5PSIuMiIvPgogICAgICAgICAgICAgICAgICAgIDxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjQiLz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+Cg==`; const ICON_2 = `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2NjIpLCBxdWFsaXR5ID0gNzAK/9sAQwAKBwcIBwYKCAgICwoKCw4YEA4NDQ4dFRYRGCMfJSQiHyIhJis3LyYpNCkhIjBBMTQ5Oz4+PiUuRElDPEg3PT47/9sAQwEKCwsODQ4cEBAcOygiKDs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7/8AAEQgCAAIAAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A9gooopDCiiigCWiiikAUUUUAR0UUUigooooAfRRRQIKKKKAFooopiCiiigBaKKKBBRRRQA2iiirJCiiigCOiiimZhRRRQAtFFFcRqFFFFAC0UUV2lhRRRQA6iiikQFFFFAEdFFFBgFFFFAD6KKKyNgooooAKKKK2NAooooAWiiikIKKKKAG0UUUxhRRRQA2iiioEFFFFAEtFFFIAooooAjooopFBRRRQA+iiigQUUUUALRRRTEFFFFAC0UUUCCiiigBtFFFWSFFFFAEdFFFMzCiiigBaKKK4jUKKKKAFooortLCiiigB1FFFIgKKKKAI6KKKDAKKKKAH0UUVkbBRRRQAUUUVsaBRRRQAtFFFIQUUUUANooopjCiiigCSiiisyQooooAbRRRVkhRRRQAtFFFABRRRQBBRRRVgFFFFAE9FFFQWFFFFAFSiiisTUKKKKAEooorAsKKKKAGUUUViUFFFFAFeiiitjYKKKKAJaKKK6jkCiiigDRoooqzMKKKKAKtFFFQaBRRRQBcooopmIUUUUAV6KKKg0CiiigCeiiiqMwooooAiooorU5QooooAmooorI6gooooAbRRRVkhRRRQAtFFFABRRRQBBRRRVgFFFFAE9FFFQWFFFFAFSiiisTUKKKKAEooorAsKKKKAGUUUViUFFFFAFeiiitjYKKKKAJaKKK6jkCiiigDRoooqzMKKKKAKtFFFQaBRRRQBcooopmIUUUUAV6KKKg0CiiigCeiiiqMwooooAiooorU5QooooAdRRRSNAooooAWiiigYUUUUANooorA3CiiigBKKKK5igooooA4uiiivNPRCiiigDNooorzzqCiiigChRRRXCdAUUUUAOoooqCwooooAWiiigzCiiigBaKKKRyhRRRQBPRRRXWAUUUUAdBRRRXqkhRRRQB01FFFeweWFFFFAE1FFFdhmFFFFAEdFFFUaBRRRQBJRRRUkBRRRQAUUUUHMFFFFAC0UUUDCiiigBtFFFYG4UUUUAJRRRXMUFFFFAHF0UUV5p6IUUUUAZtFFFeedQUUUUAUKKKK4ToCiiigB1FFFQWFFFFAC0UUUGYUUUUALRRRSOUKKKKAJ6KKK6wCiiigDoKKKK9UkKKKKAOmooor2DywooooAmooorsMwooooAjoooqjQKKKKAJKKKKkgKKKKACiiigQUUUUAFFFFcwBRRRQBVooorzDoCiiigDmqKKK809AKKKKAIaKKKkoKKKKALlFFFdhAUUUUAaNFFFegYhRRRQBo0UUV6BxhRRRQBHRRRWBkFFFFAEdFFFcB1hRRRQBz1FFFcp1hRRRQBlUUUVwnQFFFFAGfRRRWYwooooA72iiivoTyQooooA2KKKK9U80KKKKAI6KKK4SwooooAkoooruNgooooAKKKK5gCiiigCrRRRXmHQFFFFAHNUUUV5p6AUUUUAQ0UUVJQUUUUAXKKKK7CAooooA0aKKK9AxCiiigDRooor0DjCiiigCOiiisDIKKKKAI6KKK4DrCiiigDnqKKK5TrCiiigDKooorhOgKKKKAM+iiisxhRRRQB3tFFFfQnkhRRRQBsUUUV6p5oUUUUAR0UUVwlhRRRQBPRRRXYWFFFFACUUUVJYUUUUAcbRRRQbhRRRQBxVFFFIoKKKKAO4ooopiCiiigDrqKKKDnCiiigBKKKKsxCiiigBlFFFMxCiiigCSiiig7AooooAbRRRQMKKKKAFooooEFFFFAGPRRRXgncFFFFAHFUUUV4p7QUUUUAZ1FFFamYUUUUAeg0UUV7Z44UUUUAdBRRRXqHEFFFFABRRRVEhRRRQAlFFFSWFFFFAHG0UUUG4UUUUAcVRRRSKCiiigDuKKKKYgooooA66iiig5wooooASiiirMQooooAZRRRTMQooooAkooooOwKKKKAG0UUUDCiiigBaKKKBBRRRQBj0UUV4J3BRRRQBxVFFFeKe0FFFFAGdRRRWpmFFFFAHoNFFFe2eOFFFFAHQUUUV6hxBRRRQA2iiitDIKKKKAPHaKKKzJCiiigDyKiiipAKKKKAIaKKKosKKKKAGUUUUxhRRRQB3tFFFUMKKKKAPoqiiimWFFFFABRRRQAUUUUAPooopGYUUUUAR0UUVJqFFFFAHEUUUV4R6YUUUUAZ9FFFeeegFFFFAEVFFFZm4UUUUAZ1FFFcpqFFFFAF6iiitTmCiiigD0yiiivuz5sKKKKAJaKKKk5gooooA8dooorMkKKKKAPIqKKKkAooooAhoooqiwooooAZRRRTGFFFFAHe0UUVQwooooA+iqKKKZYUUUUAFFFFABRRRQA+iiikZhRRRQBHRRRUmoUUUUAcRRRRXhHphRRRQBn0UUV556AUUUUARUUUVmbhRRRQBnUUUVymoUUUUAXqKKK1OYKKKKAPTKKKK+7PmwooooA46iiisTUKKKKAPnCiiiqICiiigD2CiiiqMwooooA9hoooqiAooooA5CiiimSFFFFAHzTRRRUjCiiigD1CiiigoKKKKAPeaKKKQwooooAKKKK5yAooooA5OiiivAPTCiiigDkqKKK4jsCiiigDvKKKK9484KKKKAN2iiivSPPCiiigDJooorwD0gooooA8xooorzz0wooooA9Cooor2znCiiigDLooor1DzwooooA+cKKKKogKKKKAPYKKKKozCiiigD2GiiiqICiiigDkKKKKZIUUUUAfNNFFFSMKKKKAPUKKKKCgooooA95ooopDCiiigAooornICiiigDk6KKK8A9MKKKKAOSoooriOwKKKKAO8ooor3jzgooooA3aKKK9I88KKKKAMmiiivAPSCiiigDzGiiivPPTCiiigD0KiiivbOcKKKKAPCqKKK9U8AKKKKAMeiiimUFFFFAH1lRRRQUFFFFADaKKKZYUUUUALRRRSEFFFFAHjFFFFQAUUUUAeO0UUUyAooooA+x6KKKzGFFFFACUUUUywooooA8tooor5A+nCiiigDpKKKK7DAKKKKAOwooor6k+aCiiigBlFFFMyCiiigBtFFFcB2hRRRQBxFFFFeaegFFFFACUUUV5hYUUUUAeSUUUV9ieEFFFFAGPRRRTKCiiigD6yooooKCiiigBtFFFMsKKKKAFooopCCiiigDxiiiioAKKKKAPHaKKKZAUUUUAfY9FFFZjCiiigBKKKKZYUUUUAeW0UUV8gfThRRRQB0lFFFdhgFFFFAHYUUUV9SfNBRRRQAyiiimZBRRRQA2iiiuA7QooooA4iiiivNPQCiiigBKKKK8wsKKKKAPKaKKK+yPHCiiigDAooooAKKKKAPrOiiigAooooAWiiigAooooAKKKKACiiigDwWiiigAooooA8xooooAKKKKAPsaiiigAooooASiiisygooooA8rooor4g9wKKKKAOlooorrJCiiigDsKKKK+xPCCiiigBtFFFMyCiiigCCiiiuM7gooooA4aiiivKPaCiiigCxRRRXCc4UUUUAeTUUUV9aeKFFFFAGBRRRQAUUUUAfWdFFFABRRRQAtFFFABRRRQAUUUUAFFFFAHgtFFFABRRRQB5jRRRQAUUUUAfY1FFFABRRRQAlFFFZlBRRRQB5XRRRXxB7gUUUUAdLRRRXWSFFFFAHYUUUV9ieEFFFFADaKKKZkFFFFAEFFFFcZ3BRRRQBw1FFFeUe0FFFFAFiiiiuE5wooooAo0UUV9aeKFFFFAHgNFFFABRRRQB6tRRRQAUUUUAen0UUUAFFFFAHA0UUUAFFFFAHjFFFFABRRRQB67RRRQAUUUUAe10UUUAFFFFADaKKKZAUUUUAcxRRRXinshRRRQBxFFFFeWdoUUUUAd1RRRXonEFFFFAGvRRRXpHGFFFFAGLRRRXjHqBRRRQBwlFFFchYUUUUAel0UUV7RwBRRRQBx1FFFeoeaFFFFAHgNFFFABRRRQB6tRRRQAUUUUAen0UUUAFFFFAHA0UUUAFFFFAHjFFFFABRRRQB67RRRQAUUUUAe10UUUAFFFFADaKKKZAUUUUAcxRRRXinshRRRQBxFFFFeWdoUUUUAd1RRRXonEFFFFAGvRRRXpHGFFFFAGLRRRXjHqBRRRQBwlFFFchYUUUUAel0UUV7RwBRRRQBq0UUV6h5oUUUUAfJFFFFABRRRQBnUUUUAFFFFAGhRRRQAUUUUAR0UUUAFFFFAHplFFFABRRRQB7pRRRQAUUUUANooooAKKKKAIaKKKyOUKKKKAH0UUVkdoUUUUAc1RRRXzx7QUUUUAc7RRRXEMKKKKALdFFFdBIUUUUAZVFFFcIwooooAfRRRTICiiigDraKKK+oOcKKKKAOiooor1TzQooooA+SKKKKACiiigDOooooAKKKKANCiiigAooooAjooooAKKKKAPTKKKKACiiigD3SiiigAooooAbRRRQAUUUUAQ0UUVkcoUUUUAPooorI7QooooA5qiiivnj2gooooA52iiiuIYUUUUAW6KKK6CQooooAyqKKK4RhRRRQA+iiimQFFFFAHW0UUV9Qc4UUUUAdJRRRXqnmhRRRQBztFFFABRRRQB8r0UUUAFFFFABRRRQAUUUUAfUlFFFABRRRQB1FFFFABRRRQBXooooAKKKKAJqKKKACiiigBtFFFUSFFFFABRRRQaBRRRQBFRRRVmgUUUUAZVFFFcZ0BRRRQBxlFFFcZAUUUUAaVFFFIkKKKKANuiiiuoYUUUUAblFFFdZkFFFFAE1FFFIyCiiigDnaKKKACiiigD5XooooAKKKKACiiigAooooA+pKKKKACiiigDqKKKKACiiigCvRRRQAUUUUATUUUUAFFFFADaKKKokKKKKACiiig0CiiigCKiiirNAooooAyqKKK4zoCiiigDjKKKK4yAooooA0qKKKRIUUUUAbdFFFdQwooooA3KKKK6zIKKKKAJKKKKRkFFFFAElFFFABRRRQBytFFFABRRRQB4VRRRQAUUUUAclRRRQAUUUUAdrRRRQAUUUUAd5RRRQAUUUUAdHRRRQAUUUUAV6KKKosKKKKAMKiiisjMKKKKAOJooorrAKKKKAOIooopAFFFFAGtRRRXGIKKKKAPf6KKK6TMKKKKAOzoooqDUKKKKAEooorlLCiiigBKKKK6SQooooAkooooAKKKKAOVooooAKKKKAPCqKKKACiiigDkqKKKACiiigDtaKKKACiiigDvKKKKACiiigDo6KKKACiiigCvRRRVFhRRRQBhUUUVkZhRRRQBxNFFFdYBRRRQBxFFFFIAooooA1qKKK4xBRRRQB7/RRRXSZhRRRQB2dFFFQahRRRQAlFFFcpYUUUUAPooorpJCiiigCtRRRQAUUUUAWaKKKACiiigCvRRRQAUUUUAeV0UUUAFFFFAHl9FFFABRRRQBz9FFFABRRRQAUUUUAFFFFAElFFFQUFFFFAEdFFFUIKKKKAOgooopDCiiigD0miiigsKKKKAPYaKKKszCiiigBKKKKZAUUUUAJRRRQIKKKKAG0UUVzjCiiigCaiiitigooooArUUUUAFFFFAFmiiigAooooAr0UUUAFFFFAHldFFFABRRRQB5fRRRQAUUUUAc/RRRQAUUUUAFFFFABRRRQBJRRRUFBRRRQBHRRRVCCiiigDoKKKKQwooooA9JooooLCiiigD2GiiirMwooooASiiimQFFFFACUUUUCCiiigBtFFFc4wooooAlooorYoKKKKAFooooAKKKKAI6KKKACiiigCSiiigAooooAKKKKACiiigCGiiigAooooAbRRRQAUUUUAZdFFFABRRRQA+iiishBRRRQBq0UUVuWFFFFAEVFFFYlBRRRQBNRRRWJIUUUUAFFFFdYBRRRQAlFFFMAooooAWiiiggKKKKACiiipJCiiigB1FFFBYUUUUALRRRQAUUUUAR0UUUAFFFFAElFFFABRRRQAUUUUAFFFFAENFFFABRRRQA2iiigAooooAy6KKKACiiigB9FFFZCCiiigDVooorcsKKKKAIqKKKxKCiiigCaiiisSQooooAKKKK6wCiiigBKKKKYBRRRQAtFFFBAUUUUAFFFFSSFFFFABRRRQWFFFFAD6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigBKKKKACiiigBaKKKACiiigBtFFFABRRRQAUUUUyAooooAbRRRTNAooooAfRRRUiCiiigCOiiipMgooooAfRRRTLCiiigBKKKKZoFFFFAC0UUUwCiiigBtFFFQZhRRRQAtFFFUUFFFFAD6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigBKKKKACiiigBaKKKACiiigBtFFFABRRRQAUUUUyAooooAbRRRTNAooooAfRRRUiCiiigCOiiipMgooooAfRRRTLCiiigBKKKKZoFFFFAC0UUUwCiiigBtFFFQZhRRRQB//Z`; diff --git a/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js b/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js index f1a4c8201..18cb4f811 100644 --- a/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js +++ b/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { render, screen } from '../../widgets/utils/testUtils'; -import LegendProportion from '../../../src/widgets/legend/LegendProportion'; +import LegendProportion from '../../../src/widgets/new-legend/legend-types/LegendProportion'; import { IntlProvider } from 'react-intl'; const DEFAULT_LEGEND = { diff --git a/packages/react-ui/__tests__/widgets/legend/LegendRamp.test.js b/packages/react-ui/__tests__/widgets/legend/LegendRamp.test.js index 11d3d524c..ef9b4c325 100644 --- a/packages/react-ui/__tests__/widgets/legend/LegendRamp.test.js +++ b/packages/react-ui/__tests__/widgets/legend/LegendRamp.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { render, screen } from '../../widgets/utils/testUtils'; -import LegendRamp from '../../../src/widgets/legend/LegendRamp'; +import LegendRamp from '../../../src/widgets/new-legend/legend-types/LegendRamp'; import { getPalette } from '../../../src/utils/palette'; import { hexToRgb } from '@mui/material'; diff --git a/packages/react-ui/src/assets/icons/OpacityIcon.js b/packages/react-ui/src/assets/icons/OpacityIcon.js index 73acca8a2..437e7691a 100644 --- a/packages/react-ui/src/assets/icons/OpacityIcon.js +++ b/packages/react-ui/src/assets/icons/OpacityIcon.js @@ -1,3 +1,4 @@ +import React from 'react'; import { SvgIcon } from '@mui/material'; export default function OpacityIcon(props) { diff --git a/packages/react-ui/src/index.d.ts b/packages/react-ui/src/index.d.ts index 2418b1b68..a4606baa3 100644 --- a/packages/react-ui/src/index.d.ts +++ b/packages/react-ui/src/index.d.ts @@ -9,11 +9,12 @@ import FormulaWidgetUI from './widgets/FormulaWidgetUI/FormulaWidgetUI'; import BarWidgetUI from './widgets/BarWidgetUI/BarWidgetUI'; import HistogramWidgetUI from './widgets/HistogramWidgetUI/HistogramWidgetUI'; import PieWidgetUI from './widgets/PieWidgetUI/PieWidgetUI'; -import LegendWidgetUI, { LEGEND_TYPES } from './widgets/legend/LegendWidgetUI'; -import LegendCategories from './widgets/legend/LegendCategories'; -import LegendIcon from './widgets/legend/LegendIcon'; -import LegendProportion from './widgets/legend/LegendProportion'; -import LegendRamp from './widgets/legend/LegendRamp'; +import LegendWidgetUI from './widgets/new-legend/LegendWidgetUI'; +import LEGEND_TYPES from './widgets/new-legend/legend-types/LegendTypes' +import LegendCategories from './widgets/new-legend/legend-types/LegendCategories'; +import LegendIcon from './widgets/new-legend/legend-types/LegendIcon'; +import LegendProportion from './widgets/new-legend/legend-types/LegendProportion'; +import LegendRamp from './widgets/new-legend/legend-types/LegendRamp'; import ScatterPlotWidgetUI from './widgets/ScatterPlotWidgetUI/ScatterPlotWidgetUI'; import TimeSeriesWidgetUI from './widgets/TimeSeriesWidgetUI/TimeSeriesWidgetUI'; import { @@ -136,4 +137,4 @@ export { Alert, AlertProps, CartoAlertSeverity -}; +}; \ No newline at end of file diff --git a/packages/react-ui/src/index.js b/packages/react-ui/src/index.js index 6af1c5de9..2feed3764 100644 --- a/packages/react-ui/src/index.js +++ b/packages/react-ui/src/index.js @@ -5,11 +5,12 @@ import FormulaWidgetUI from './widgets/FormulaWidgetUI/FormulaWidgetUI'; import BarWidgetUI from './widgets/BarWidgetUI/BarWidgetUI'; import HistogramWidgetUI from './widgets/HistogramWidgetUI/HistogramWidgetUI'; import PieWidgetUI from './widgets/PieWidgetUI/PieWidgetUI'; -import LegendWidgetUI, { LEGEND_TYPES } from './widgets/legend/LegendWidgetUI'; -import LegendCategories from './widgets/legend/LegendCategories'; -import LegendIcon from './widgets/legend/LegendIcon'; -import LegendProportion from './widgets/legend/LegendProportion'; -import LegendRamp from './widgets/legend/LegendRamp'; +import LegendWidgetUI from './widgets/new-legend/LegendWidgetUI'; +import LEGEND_TYPES from './widgets/new-legend/legend-types/LegendTypes'; +import LegendCategories from './widgets/new-legend/legend-types/LegendCategories'; +import LegendIcon from './widgets/new-legend/legend-types/LegendIcon'; +import LegendProportion from './widgets/new-legend/legend-types/LegendProportion'; +import LegendRamp from './widgets/new-legend/legend-types/LegendRamp'; import ScatterPlotWidgetUI from './widgets/ScatterPlotWidgetUI/ScatterPlotWidgetUI'; import TimeSeriesWidgetUI from './widgets/TimeSeriesWidgetUI/TimeSeriesWidgetUI'; import FeatureSelectionWidgetUI from './widgets/FeatureSelectionWidgetUI/FeatureSelectionWidgetUI'; diff --git a/packages/react-ui/src/localization/en.js b/packages/react-ui/src/localization/en.js index 6ab431cea..b837d4a07 100644 --- a/packages/react-ui/src/localization/en.js +++ b/packages/react-ui/src/localization/en.js @@ -50,7 +50,7 @@ const locales = { greaterThan: 'greater than', and: 'and', zoomNote: 'Note: this layer will display at zoom levels', - notSupported: 'legend type not supported', + notSupported: 'is not a known legend type', subtitles: { proportion: 'Radius range by', icon: 'Icon based on', diff --git a/packages/react-ui/src/types.d.ts b/packages/react-ui/src/types.d.ts index 5935ca3e6..4ff79b0ac 100644 --- a/packages/react-ui/src/types.d.ts +++ b/packages/react-ui/src/types.d.ts @@ -7,6 +7,21 @@ export { AccordionGroupProps } from './components/molecules/AccordionGroup'; export { UploadFieldProps } from './components/molecules/UploadField/UploadField'; export { UploadFieldBaseProps } from './components/molecules/UploadField/UploadFieldBase'; export { AppBarProps } from './components/organisms/AppBar/AppBar'; +export { + LegendBins, + LegendCategories, + LegendIcons, + LegendRamp, + LegendProportion, + LegendWidgetUIProps, + LegendSelectConfig, + CustomLegendComponent, + LegendLayerData, + LegendLayerVariableData, + LegendLayerVariableBase, + LegendColors, + LegendNumericLabels +} from './widgets/new-legend/LegendWidgetUI' export type WrapperWidgetUI = { title: string; @@ -203,36 +218,6 @@ export type FeatureSelectionUIToggleButton = { tooltipPlacement?: 'bottom' | 'left' | 'right' | 'top'; }; -// Legends -export type LegendCategories = { - legend: { - labels?: (string | number)[]; - colors?: string | string[] | number[][]; - isStrokeColor?: boolean; - }; -}; - -export type LegendIcon = { - legend: { - labels?: string[]; - icons?: string[]; - }; -}; - -export type LegendProportion = { - legend: { - labels?: (number | string)[]; - }; -}; - -export type LegendRamp = { - isContinuous?: boolean; - legend: { - labels?: (number | string)[]; - colors?: string | string[] | number[][]; - }; -}; - export type AnimationOptions = { duration?: number; animateOnMount?: boolean; diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/legend/LegendWidgetUI.js index 42211fcbb..3a7bc8247 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.js @@ -1,10 +1,10 @@ import React, { createRef, Fragment } from 'react'; import { Box, Button, Collapse, Divider, Grid, styled, SvgIcon } from '@mui/material'; import LegendWrapper from './LegendWrapper'; -import LegendCategories from './LegendCategories'; -import LegendIcon from './LegendIcon'; -import LegendRamp from './LegendRamp'; -import LegendProportion from './LegendProportion'; +import LegendCategories from '../new-legend/legend-types/LegendCategories'; +import LegendIcon from '../new-legend/legend-types/LegendIcon'; +import LegendRamp from '../new-legend/legend-types/LegendRamp'; +import LegendProportion from '../new-legend/legend-types/LegendProportion'; import PropTypes from 'prop-types'; import Typography from '../../components/atoms/Typography'; diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayer.js b/packages/react-ui/src/widgets/new-legend/LegendLayer.js index 837b8992f..ee636a8a5 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayer.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayer.js @@ -1,10 +1,10 @@ +import React, { useMemo, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import { Box, Collapse, IconButton, Tooltip, Typography } from '@mui/material'; import EyeIcon from '@mui/icons-material/VisibilityOutlined'; import EyeOffIcon from '@mui/icons-material/VisibilityOffOutlined'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; -import { useMemo, useRef, useState } from 'react'; import { styles } from './LegendWidgetUI.styles'; import LegendOpacityControl from './LegendOpacityControl'; import LegendLayerTitle from './LegendLayerTitle'; @@ -17,8 +17,8 @@ const EMPTY_OBJ = {}; /** * Receives configuration options, send change events and renders a legend item * @param {object} props - * @param {Object.<string, import('../legend/LegendWidgetUI').CustomLegendComponent>} props.customLegendTypes - Allow to customise by default legend types that can be rendered. - * @param {import('../legend/LegendWidgetUI').LegendLayerData} props.layer - Layer object from redux store. + * @param {Object.<string, import('./LegendWidgetUI').CustomLegendComponent>} props.customLegendTypes - Allow to customise by default legend types that can be rendered. + * @param {import('./LegendWidgetUI').LegendLayerData} props.layer - Layer object from redux store. * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} props.onChangeCollapsed - Callback function for layer visibility change. * @param {({ id, opacity }: { id: string, opacity: number }) => void} props.onChangeOpacity - Callback function for layer opacity change. * @param {({ id, visible }: { id: string, visible: boolean }) => void} props.onChangeVisibility - Callback function for layer collapsed state change. @@ -68,7 +68,7 @@ export default function LegendLayer({ layerMinZoom: layer.minZoom, layerMaxZoom: layer.maxZoom }); - const helperText = layer.helperText ?? showZoomNote ? zoomHelperText : ''; + const helperText = layer.helperText ?? (showZoomNote ? zoomHelperText : ''); const legendLayerVariables = useMemo(() => { if (!layer.legend) { @@ -162,7 +162,7 @@ export default function LegendLayer({ > {legendLayerVariables.map((legend, index) => ( <LegendLayerVariable - key={legend.type} + key={`${legend.type}-${index}`} legend={legend} layer={layer} customLegendTypes={customLegendTypes} diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js index 3593010a5..a944a229b 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js +++ b/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js @@ -1,24 +1,25 @@ +import React from 'react'; import { Box, ListItemText, MenuItem, Select } from '@mui/material'; -import LegendCategories from '../legend/LegendCategories'; -import LegendIcon from '../legend/LegendIcon'; -import LegendRamp from '../legend/LegendRamp'; -import LegendProportion from '../legend/LegendProportion'; -import { LEGEND_TYPES } from '../legend/LegendWidgetUI'; +import LegendCategories from './legend-types/LegendCategories'; +import LegendIcon from './legend-types/LegendIcon'; +import LegendRamp from './legend-types/LegendRamp'; +import LegendProportion from './legend-types/LegendProportion'; +import LEGEND_TYPES from './legend-types/LegendTypes'; import { useIntl } from 'react-intl'; import useImperativeIntl from '../../hooks/useImperativeIntl'; import Typography from '../../components/atoms/Typography'; const legendTypeMap = { [LEGEND_TYPES.CATEGORY]: LegendCategories, - [LEGEND_TYPES.ICON]: LegendIcon, - [LEGEND_TYPES.BINS]: LegendRamp, [LEGEND_TYPES.PROPORTION]: LegendProportion, - [LEGEND_TYPES.CONTINUOUS_RAMP]: LegendRamp + [LEGEND_TYPES.ICON]: LegendIcon, + [LEGEND_TYPES.BINS]: (props) => <LegendRamp {...props} isContinuous={false} />, + [LEGEND_TYPES.CONTINUOUS_RAMP]: (props) => <LegendRamp {...props} isContinuous={true} /> }; /** * @param {object} props - * @param {import('../legend/LegendWidgetUI').LegendLayerVariableBase} props.legend - legend variable data. + * @param {import('./LegendWidgetUI').LegendLayerVariableBase} props.legend - legend variable data. * @returns {React.ReactNode} */ function LegendUnknown({ legend }) { @@ -31,14 +32,13 @@ function LegendUnknown({ legend }) { return ( <Typography variant='body2' color='textSecondary' component='p'> - "{legend.type}"{' '} - {intlConfig.formatMessage({ id: 'c4r.widgets.legend.notSupported' })} + {legend.type} {intlConfig.formatMessage({ id: 'c4r.widgets.legend.notSupported' })}. </Typography> ); } /** - * @param {import('../legend/LegendWidgetUI').LegendLayerVariableData} legend - legend variable data. + * @param {import('./LegendWidgetUI').LegendLayerVariableData} legend - legend variable data. * @returns {string} */ function getLegendSubtitle(legend) { @@ -56,9 +56,9 @@ function getLegendSubtitle(legend) { /** * @param {object} props - * @param {import('../legend/LegendWidgetUI').LegendLayerData} props.layer - Layer object from redux store. - * @param {import('../legend/LegendWidgetUI').LegendLayerVariableData} props.legend - legend variable data. - * @param {Object.<string, import('../legend/LegendWidgetUI').CustomLegendComponent>} props.customLegendTypes - Map from legend type to legend component that allows to customise additional legend types that can be rendered. + * @param {import('./LegendWidgetUI').LegendLayerData} props.layer - Layer object from redux store. + * @param {import('./LegendWidgetUI').LegendLayerVariableData} props.legend - legend variable data. + * @param {Object.<string, import('./LegendWidgetUI').CustomLegendComponent>} props.customLegendTypes - Map from legend type to legend component that allows to customise additional legend types that can be rendered. * @param {(selection: unknown) => void} props.onChangeSelection - Callback function for legend options change. * @returns {React.ReactNode} */ diff --git a/packages/react-ui/src/widgets/new-legend/LegendOpacityControl.js b/packages/react-ui/src/widgets/new-legend/LegendOpacityControl.js index 7c6b6da28..4c17074b0 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendOpacityControl.js +++ b/packages/react-ui/src/widgets/new-legend/LegendOpacityControl.js @@ -1,3 +1,4 @@ +import React from 'react'; import { Box, IconButton, diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.d.ts similarity index 56% rename from packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts rename to packages/react-ui/src/widgets/new-legend/LegendWidgetUI.d.ts index 06bfea0b2..fa9aa5051 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.d.ts @@ -8,6 +8,26 @@ export enum LEGEND_TYPES { PROPORTION = 'proportion', } +export type LegendWidgetUIProps = { + customLegendTypes?: Record<string, CustomLegendComponent>; + layers?: LegendLayerData[]; + collapsed?: boolean; + onChangeCollapsed?: (collapsed: boolean) => void; + onChangeLegendRowCollapsed?: ({ id, collapsed }: { id: string, collapsed: boolean }) => void; + onChangeOpacity?: ({ id, opacity }: { id: string, opacity: number }) => void + onChangeVisibility?: ({ id, visible }: { id: string, visible: boolean }) => void + onChangeSelection?: ({ id, index, selection }: { id: string, index: number, selection: unknown }) => void + title?: string + position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' + maxZoom?: number + minZoom?: number + currentZoom?: number + isMobile?: boolean +} + +declare const LegendWidgetUI: (props: LegendWidgetUIProps) => React.ReactNode; +export default LegendWidgetUI; + export type LegendLayerData = { id: string; title?: string; @@ -24,37 +44,37 @@ export type LegendLayerData = { }; export type LegendLayerVariableBase = { - type: LEGEND_TYPES; + type: LEGEND_TYPES | string; select: LegendSelectConfig attr?: string; // subtitle to show below the legend item toggle when expanded } export type LegendLayerVariableData = LegendLayerVariableBase & LegendType; -type LegendType = LegendBins | LegendRamp | LegendIcons | LegendCategories | LegendProportion; +export type LegendType = LegendBins | LegendRamp | LegendIcons | LegendCategories | LegendProportion; -type LegendColors = string | string[] | number[][]; -type LegendNumericLabels = number[] | { label: string; value: number }[]; +export type LegendColors = string | string[] | number[][]; +export type LegendNumericLabels = number[] | { label: string; value: number }[]; -type LegendBins = { +export type LegendBins = { colors: LegendColors labels: LegendNumericLabels } -type LegendRamp = { +export type LegendRamp = { colors: LegendColors labels: LegendNumericLabels } -type LegendIcons = { +export type LegendIcons = { icons: string[] labels: string[] } -type LegendCategories = { +export type LegendCategories = { colors: LegendColors labels: string[] | number[] isStrokeColor?: boolean customMarkers?: string | string[] maskedMarkers?: boolean } -type LegendProportion = { +export type LegendProportion = { labels: [number, number] } diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js index c7050d310..6f53fb1fe 100644 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js @@ -1,3 +1,4 @@ +import React from 'react'; import PropTypes from 'prop-types'; import { Box, @@ -21,15 +22,14 @@ const EMPTY_ARR = []; /** * @param {object} props - * @param {Object.<string, import('../legend/LegendWidgetUI').CustomLegendComponent>} [props.customLegendTypes] - Allow to customise by default legend types that can be rendered. - * @param {import('../legend/LegendWidgetUI').LegendLayerData[]} [props.layers] - Array of layer objects from redux store. + * @param {Object.<string, import('./LegendWidgetUI').CustomLegendComponent>} [props.customLegendTypes] - Allow to customise by default legend types that can be rendered. + * @param {import('./LegendWidgetUI').LegendLayerData[]} [props.layers] - Array of layer objects from redux store. * @param {boolean} [props.collapsed] - Collapsed state for whole legend widget. - * @param {(collapsed: boolean) => void} props.onChangeCollapsed - Callback function for collapsed state change. - * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} props.onChangeLegendRowCollapsed - Callback function for layer visibility change. - * @param {({ id, opacity }: { id: string, opacity: number }) => void} props.onChangeOpacity - Callback function for layer opacity change. - * @param {({ id, visible }: { id: string, visible: boolean }) => void} props.onChangeVisibility - Callback function for layer collapsed state change. + * @param {(collapsed: boolean) => void} [props.onChangeCollapsed] - Callback function for collapsed state change. + * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} [props.onChangeLegendRowCollapsed] - Callback function for layer visibility change. + * @param {({ id, opacity }: { id: string, opacity: number }) => void} [props.onChangeOpacity] - Callback function for layer opacity change. + * @param {({ id, visible }: { id: string, visible: boolean }) => void} [props.onChangeVisibility] - Callback function for layer collapsed state change. * @param {({ id, index, selection }: { id: string, index: number, selection: unknown }) => void} props.onChangeSelection - Callback function for layer variable selection change. - * @param {string[]} [props.layerOrder] - Array of layer identifiers. Defines the order of layer legends. [] by default. * @param {string} [props.title] - Title of the toggle button when widget is open. * @param {'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'} [props.position] - Position of the widget. * @param {number} [props.maxZoom] - Global maximum zoom level for the map. @@ -38,16 +38,15 @@ const EMPTY_ARR = []; * @param {boolean} [props.isMobile] - Whether the widget is displayed on a mobile device. * @returns {React.ReactNode} */ -function NewLegendWidgetUI({ +function LegendWidgetUI({ customLegendTypes = EMPTY_OBJ, layers = EMPTY_ARR, - collapsed = true, + collapsed = false, onChangeCollapsed = EMPTY_FN, onChangeVisibility = EMPTY_FN, onChangeOpacity = EMPTY_FN, onChangeLegendRowCollapsed = EMPTY_FN, onChangeSelection = EMPTY_FN, - layerOrder, title, position = 'bottom-right', maxZoom = 21, @@ -57,11 +56,12 @@ function NewLegendWidgetUI({ } = {}) { const intl = useIntl(); const intlConfig = useImperativeIntl(intl); + const isSingle = layers.length === 1; const rootSx = { ...styles[position], ...styles.root, - width: collapsed ? undefined : LEGEND_WIDTH + width: collapsed || isMobile ? undefined : LEGEND_WIDTH }; const legendToggleHeader = ( @@ -81,80 +81,112 @@ function NewLegendWidgetUI({ </Tooltip> </Box> ); + const legendToggleButton = ( + <Tooltip title={intlConfig.formatMessage({ id: 'c4r.widgets.legend.open' })}> + <IconButton aria-label={title} onClick={() => onChangeCollapsed(false)}> + <LayerIcon /> + </IconButton> + </Tooltip> + ); + + if (isSingle) { + return ( + <Paper elevation={3} sx={rootSx}> + <Box style={styles.legendItemList}> + <LegendLayer + layer={layers[0]} + onChangeCollapsed={onChangeLegendRowCollapsed} + onChangeOpacity={onChangeOpacity} + onChangeVisibility={onChangeVisibility} + onChangeSelection={onChangeSelection} + maxZoom={maxZoom} + minZoom={minZoom} + currentZoom={currentZoom} + customLegendTypes={customLegendTypes} + /> + </Box> + </Paper> + ); + } return ( <Paper elevation={3} sx={rootSx}> - {collapsed ? ( - <Tooltip title={intlConfig.formatMessage({ id: 'c4r.widgets.legend.open' })}> - <IconButton onClick={() => onChangeCollapsed(false)}> - <LayerIcon /> - </IconButton> - </Tooltip> - ) : isMobile ? null : ( - legendToggleHeader - )} {isMobile ? ( - <Drawer anchor='bottom' open={!collapsed} onClose={() => onChangeCollapsed(true)}> - {legendToggleHeader} - <Box style={styles.legendItemList}> - {layers.map((l) => ( - <LegendLayer - key={l.id} - layer={l} - onChangeCollapsed={onChangeLegendRowCollapsed} - onChangeOpacity={onChangeOpacity} - onChangeVisibility={onChangeVisibility} - onChangeSelection={onChangeSelection} - maxZoom={maxZoom} - minZoom={minZoom} - currentZoom={currentZoom} - customLegendTypes={customLegendTypes} - /> - ))} - </Box> - </Drawer> + <> + {legendToggleButton} + <Drawer + anchor='bottom' + open={!collapsed} + onClose={() => onChangeCollapsed(true)} + > + {legendToggleHeader} + <Box style={styles.legendItemList}> + {layers.map((l) => ( + <LegendLayer + key={l.id} + layer={l} + onChangeCollapsed={onChangeLegendRowCollapsed} + onChangeOpacity={onChangeOpacity} + onChangeVisibility={onChangeVisibility} + onChangeSelection={onChangeSelection} + maxZoom={maxZoom} + minZoom={minZoom} + currentZoom={currentZoom} + customLegendTypes={customLegendTypes} + /> + ))} + </Box> + </Drawer> + </> ) : ( - <Box sx={{ ...styles.legendItemList, width: collapsed ? 0 : undefined }}> - <Collapse unmountOnExit in={!collapsed} timeout={500}> - {layers.map((l) => ( - <LegendLayer - key={l.id} - layer={l} - onChangeCollapsed={onChangeLegendRowCollapsed} - onChangeOpacity={onChangeOpacity} - onChangeVisibility={onChangeVisibility} - onChangeSelection={onChangeSelection} - maxZoom={maxZoom} - minZoom={minZoom} - currentZoom={currentZoom} - customLegendTypes={customLegendTypes} - /> - ))} - </Collapse> - </Box> + <> + {collapsed ? legendToggleButton : legendToggleHeader} + <Box sx={{ ...styles.legendItemList, width: collapsed ? 0 : undefined }}> + <Collapse unmountOnExit in={!collapsed} timeout={500}> + {layers.map((l) => ( + <LegendLayer + key={l.id} + layer={l} + onChangeCollapsed={onChangeLegendRowCollapsed} + onChangeOpacity={onChangeOpacity} + onChangeVisibility={onChangeVisibility} + onChangeSelection={onChangeSelection} + maxZoom={maxZoom} + minZoom={minZoom} + currentZoom={currentZoom} + customLegendTypes={customLegendTypes} + /> + ))} + </Collapse> + </Box> + </> )} </Paper> ); } -NewLegendWidgetUI.defaultProps = { - layers: [], - customLegendTypes: {}, - collapsed: true, +LegendWidgetUI.defaultProps = { + layers: EMPTY_ARR, + customLegendTypes: EMPTY_OBJ, + collapsed: false, title: 'Layers', - position: 'bottom-right' + position: 'bottom-right', + onChangeCollapsed: EMPTY_FN, + onChangeLegendRowCollapsed: EMPTY_FN, + onChangeVisibility: EMPTY_FN, + onChangeOpacity: EMPTY_FN, + onChangeSelection: EMPTY_FN }; -NewLegendWidgetUI.propTypes = { +LegendWidgetUI.propTypes = { customLegendTypes: PropTypes.objectOf(PropTypes.func), layers: PropTypes.array, - collapsed: PropTypes.bool.isRequired, - onChangeCollapsed: PropTypes.func.isRequired, - onChangeLegendRowCollapsed: PropTypes.func.isRequired, - onChangeVisibility: PropTypes.func.isRequired, - onChangeOpacity: PropTypes.func.isRequired, - onChangeSelection: PropTypes.func.isRequired, - layerOrder: PropTypes.arrayOf(PropTypes.string), + collapsed: PropTypes.bool, + onChangeCollapsed: PropTypes.func, + onChangeLegendRowCollapsed: PropTypes.func, + onChangeVisibility: PropTypes.func, + onChangeOpacity: PropTypes.func, + onChangeSelection: PropTypes.func, title: PropTypes.string, position: PropTypes.oneOf(['top-left', 'top-right', 'bottom-left', 'bottom-right']), maxZoom: PropTypes.number, @@ -163,4 +195,4 @@ NewLegendWidgetUI.propTypes = { isMobile: PropTypes.bool }; -export default NewLegendWidgetUI; +export default LegendWidgetUI; diff --git a/packages/react-ui/src/widgets/legend/LegendCategories.js b/packages/react-ui/src/widgets/new-legend/legend-types/LegendCategories.js similarity index 91% rename from packages/react-ui/src/widgets/legend/LegendCategories.js rename to packages/react-ui/src/widgets/new-legend/legend-types/LegendCategories.js index 8d5851339..a9ff6faec 100644 --- a/packages/react-ui/src/widgets/legend/LegendCategories.js +++ b/packages/react-ui/src/widgets/new-legend/legend-types/LegendCategories.js @@ -1,13 +1,13 @@ import React from 'react'; import { Box, Tooltip, styled } from '@mui/material'; -import { getPalette } from '../../utils/palette'; +import { getPalette } from '../../../utils/palette'; import PropTypes from 'prop-types'; -import LegendLayerTitle from '../new-legend/LegendLayerTitle'; -import { styles } from '../new-legend/LegendWidgetUI.styles'; +import LegendLayerTitle from '../LegendLayerTitle'; +import { styles } from '../LegendWidgetUI.styles'; /** * @param {object} props - * @param {import('../legend/LegendWidgetUI').LegendLayerVariableBase & import('../legend/LegendWidgetUI').LegendCategories} props.legend - legend variable data. + * @param {import('../LegendWidgetUI').LegendLayerVariableBase & import('../LegendWidgetUI').LegendCategories} props.legend - legend variable data. * @returns {React.ReactNode} */ function LegendCategories({ legend }) { diff --git a/packages/react-ui/src/widgets/legend/LegendIcon.js b/packages/react-ui/src/widgets/new-legend/legend-types/LegendIcon.js similarity index 75% rename from packages/react-ui/src/widgets/legend/LegendIcon.js rename to packages/react-ui/src/widgets/new-legend/legend-types/LegendIcon.js index da2bfdd26..2fc7fb645 100644 --- a/packages/react-ui/src/widgets/legend/LegendIcon.js +++ b/packages/react-ui/src/widgets/new-legend/legend-types/LegendIcon.js @@ -1,13 +1,13 @@ import React from 'react'; import { Box } from '@mui/material'; import PropTypes from 'prop-types'; -import { ICON_SIZE_MEDIUM } from '../../theme/themeConstants'; -import { styles } from '../new-legend/LegendWidgetUI.styles'; -import LegendLayerTitle from '../new-legend/LegendLayerTitle'; +import { ICON_SIZE_MEDIUM } from '../../../theme/themeConstants'; +import { styles } from '../LegendWidgetUI.styles'; +import LegendLayerTitle from '../LegendLayerTitle'; /** * @param {object} props - * @param {import('../legend/LegendWidgetUI').LegendLayerVariableBase & import('../legend/LegendWidgetUI').LegendIcons} props.legend - legend variable data. + * @param {import('../LegendWidgetUI').LegendLayerVariableBase & import('../LegendWidgetUI').LegendIcons} props.legend - legend variable data. * @returns {React.ReactNode} */ function LegendIcon({ legend }) { diff --git a/packages/react-ui/src/widgets/legend/LegendProportion.js b/packages/react-ui/src/widgets/new-legend/legend-types/LegendProportion.js similarity index 92% rename from packages/react-ui/src/widgets/legend/LegendProportion.js rename to packages/react-ui/src/widgets/new-legend/legend-types/LegendProportion.js index 273f00f4a..5ddbeffb0 100644 --- a/packages/react-ui/src/widgets/legend/LegendProportion.js +++ b/packages/react-ui/src/widgets/new-legend/legend-types/LegendProportion.js @@ -1,9 +1,9 @@ import React from 'react'; import { Box, styled } from '@mui/material'; import PropTypes from 'prop-types'; -import Typography from '../../components/atoms/Typography'; +import Typography from '../../../components/atoms/Typography'; import { useIntl } from 'react-intl'; -import useImperativeIntl from '../../hooks/useImperativeIntl'; +import useImperativeIntl from '../../../hooks/useImperativeIntl'; const sizes = { 0: 12, @@ -58,7 +58,7 @@ const LabelList = styled(Box)(() => ({ /** * @param {object} props - * @param {import('../legend/LegendWidgetUI').LegendLayerVariableBase & import('../legend/LegendWidgetUI').LegendProportion} props.legend - legend variable data. + * @param {import('../LegendWidgetUI').LegendLayerVariableBase & import('../LegendWidgetUI').LegendProportion} props.legend - legend variable data. * @returns {React.ReactNode} */ function LegendProportion({ legend }) { diff --git a/packages/react-ui/src/widgets/legend/LegendRamp.js b/packages/react-ui/src/widgets/new-legend/legend-types/LegendRamp.js similarity index 93% rename from packages/react-ui/src/widgets/legend/LegendRamp.js rename to packages/react-ui/src/widgets/new-legend/legend-types/LegendRamp.js index 1008fcf2a..fd65c0296 100644 --- a/packages/react-ui/src/widgets/legend/LegendRamp.js +++ b/packages/react-ui/src/widgets/new-legend/legend-types/LegendRamp.js @@ -1,13 +1,13 @@ import { Box, Tooltip, styled } from '@mui/material'; import PropTypes from 'prop-types'; import React from 'react'; -import Typography from '../../components/atoms/Typography'; -import { getPalette } from '../../utils/palette'; +import Typography from '../../../components/atoms/Typography'; +import { getPalette } from '../../../utils/palette'; import LegendProportion, { getMinMax } from './LegendProportion'; /** * @param {object} props - * @param {import('../legend/LegendWidgetUI').LegendLayerVariableBase & import('../legend/LegendWidgetUI').LegendRamp} props.legend - legend variable data. + * @param {import('../LegendWidgetUI').LegendLayerVariableBase & import('../LegendWidgetUI').LegendRamp} props.legend - legend variable data. * @param {boolean} [props.isContinuous] - If the legend is continuous. * @returns {React.ReactNode} */ diff --git a/packages/react-ui/src/widgets/new-legend/legend-types/LegendTypes.js b/packages/react-ui/src/widgets/new-legend/legend-types/LegendTypes.js new file mode 100644 index 000000000..4ee408770 --- /dev/null +++ b/packages/react-ui/src/widgets/new-legend/legend-types/LegendTypes.js @@ -0,0 +1,8 @@ +const LEGEND_TYPES = { + CATEGORY: 'category', + ICON: 'icon', + CONTINUOUS_RAMP: 'continuous_ramp', + BINS: 'bins', + PROPORTION: 'proportion' +}; +export default LEGEND_TYPES; diff --git a/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js b/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js deleted file mode 100644 index 51c9ae469..000000000 --- a/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js +++ /dev/null @@ -1,286 +0,0 @@ -import React, { useState } from 'react'; -import LegendWidgetUI from '../../../src/widgets/legend/LegendWidgetUI'; -import { IntlProvider } from 'react-intl'; - -const options = { - title: 'Widgets/LegendWidgetUI', - component: LegendWidgetUI, - argTypes: { - layers: { - defaultValue: [], - control: { - type: 'array' - } - }, - collapsed: { - defaultValue: false, - control: { - type: 'boolean' - } - } - }, - parameters: { - docs: { - source: { - type: 'auto' - } - } - } -}; -export default options; - -const Widget = (props) => ( - <IntlProvider locale='en'> - <LegendWidgetUI {...props} /> - </IntlProvider> -); - -const Template = ({ ...args }) => { - return ( - <Widget {...args}> - <div>Your Content</div> - </Widget> - ); -}; - -const LegendTemplate = () => { - const layers = [ - { - id: 0, - title: 'Single Layer', - visible: true, - legend: { - children: <div>Your Content</div> - } - } - ]; - return <Widget layers={layers}></Widget>; -}; - -const LegendNotFoundTemplate = () => { - const layers = [ - { - id: 0, - title: 'Single Layer', - visible: true - } - ]; - return <Widget layers={layers}></Widget>; -}; - -const LegendWithOpacityTemplate = () => { - const layers = [ - { - id: 0, - title: 'Single Layer', - visible: true, - showOpacityControl: true, - opacity: 0.5, - legend: { - children: <div>Your Content</div> - } - } - ]; - return <Widget layers={layers}></Widget>; -}; - -const LegendMultiTemplate = () => { - const layers = [ - { - id: 0, - title: 'Multi Layer', - visible: true, - legend: { - children: <div>Your Content</div> - } - }, - { - id: 1, - title: 'Multi Layer', - visible: false, - collapsible: false, - legend: { - children: <div>Your Content</div> - } - } - ]; - return <Widget layers={layers}></Widget>; -}; - -const LegendMultiTemplateCollapsed = () => { - const [collapsed, setCollapsed] = useState(true); - - const layers = [ - { - id: 0, - title: 'Multi Layer', - visible: true, - legend: { - children: <div>Your Content</div> - } - }, - { - id: 1, - title: 'Multi Layer', - visible: false, - collapsible: false, - legend: { - children: <div>Your Content</div> - } - } - ]; - - return ( - <Widget - layers={layers} - collapsed={collapsed} - onChangeCollapsed={({ collapsed }) => setCollapsed(collapsed)} - /> - ); -}; - -const categoryLegend = { - type: 'category', - note: 'lorem', - colors: ['#000', '#00F', '#0F0'], - labels: ['Category 1', 'Category 2', 'Category 3'] -}; - -const LegendCategoriesTemplate = () => { - const layers = [ - { - id: 0, - title: 'Category Layer', - visible: true, - legend: categoryLegend - } - ]; - return <Widget layers={layers}></Widget>; -}; - -const LegendCategoriesStrokeTemplate = () => { - const layers = [ - { - id: 0, - title: 'Category Layer as stroke', - visible: true, - legend: { - ...categoryLegend, - isStrokeColor: true - } - } - ]; - return <Widget layers={layers}></Widget>; -}; - -const LegendIconTemplate = () => { - const layers = [ - { - id: 0, - title: 'Icon Layer', - visible: true, - legend: { - type: 'icon', - labels: ['Icon 1', 'Icon 2', 'Icon 3'], - icons: [ - '/static/media/storybook/assets/carto-symbol.svg', - '/static/media/storybook/assets/carto-symbol.svg', - '/static/media/storybook/assets/carto-symbol.svg' - ] - } - } - ]; - return <Widget layers={layers}></Widget>; -}; - -const LegendRampTemplate = () => { - const layersDiscontinuous = [ - { - id: 0, - title: 'Ramp Layer', - visible: true, - legend: { - type: 'bins', - colors: ['#000', '#00F', '#0F0', '#F00'], - labels: [100, 200, 300] - } - } - ]; - - const layersContinuous = [ - { - id: 0, - title: 'Ramp Layer', - visible: true, - legend: { - type: 'continuous_ramp', - colors: ['#000', '#00F', '#0F0', '#F00'], - labels: [100, 200, 300] - } - } - ]; - - return ( - <> - <Widget layers={layersDiscontinuous}></Widget> - <Widget layers={layersContinuous}></Widget> - </> - ); -}; - -const LegendProportionTemplate = () => { - const layers = [ - { - id: 0, - title: 'Proportion Layer', - visible: true, - legend: { - type: 'proportion', - labels: [100, 500] - // avg: 450 - } - } - ]; - return <Widget layers={layers}></Widget>; -}; - -const LegendCustomTemplate = () => { - const layers = [ - { - id: 0, - title: 'Single Layer', - visible: true, - legend: { - children: <div>Legend custom</div> - } - } - ]; - return <Widget layers={layers}></Widget>; -}; - -const LegendNoChildrenTemplate = () => { - const layers = [ - { - id: 0, - title: 'Single Layer', - visible: true, - legend: {} - } - ]; - return <Widget layers={layers}></Widget>; -}; - -export const Playground = Template.bind({}); - -export const SingleLayer = LegendTemplate.bind({}); -export const MultiLayer = LegendMultiTemplate.bind({}); -export const MultiLayerCollapsed = LegendMultiTemplateCollapsed.bind({}); -export const NotFound = LegendNotFoundTemplate.bind({}); -export const LegendWithOpacityControl = LegendWithOpacityTemplate.bind({}); -export const Categories = LegendCategoriesTemplate.bind({}); -export const CategoriesAsStroke = LegendCategoriesStrokeTemplate.bind({}); -export const Icon = LegendIconTemplate.bind({}); -export const Ramp = LegendRampTemplate.bind({}); -export const Proportion = LegendProportionTemplate.bind({}); -export const Custom = LegendCustomTemplate.bind({}); -export const NoChildren = LegendNoChildrenTemplate.bind({}); diff --git a/packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js b/packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js index 7699be63d..42651cbe9 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js @@ -20,12 +20,6 @@ const options = { type: 'array' } }, - layerOrder: { - defaultValue: [], - control: { - type: 'array' - } - }, position: { defaultValue: 'top-left', options: ['bottom-left', 'bottom-right', 'top-left', 'top-right'], @@ -78,6 +72,9 @@ function useLegendState(args) { return { collapsed, setCollapsed, layers, dispatch }; } +/** + * @param {Parameters<LegendWidgetUI>[0]} args + */ const Template = ({ ...args }) => { const { collapsed, setCollapsed, layers, dispatch } = useLegendState(args); @@ -96,3 +93,247 @@ const Template = ({ ...args }) => { ); }; export const Playground = Template.bind({}); + +const LegendTemplate = () => { + const layers = [ + { + id: 0, + title: 'Single Layer', + visible: true, + legend: { + type: 'category', + labels: ['Category 1', 'Category 2', 'Category 3'], + colors: ['#000', '#00F', '#0F0'] + } + } + ]; + return <Template layers={layers} />; +}; + +const LegendNotFoundTemplate = () => { + const layers = [ + { + id: 0, + title: 'Single Layer', + visible: true + } + ]; + return <Template layers={layers} />; +}; + +const LegendWithoutOpacityTemplate = () => { + const layers = [ + { + id: 0, + title: 'Single Layer', + visible: true, + showOpacityControl: false, + opacity: 0.5, + legend: { + type: 'category', + labels: ['Category 1'], + colors: ['#faa'] + } + } + ]; + return <Template layers={layers} />; +}; + +const LegendMultiTemplate = () => { + const layers = [ + { + id: 0, + title: 'Layer 1', + visible: true, + legend: { + type: 'category', + labels: ['Category 1'], + colors: ['#faa'] + } + }, + { + id: 1, + title: 'Layer 2', + visible: false, + collapsible: false, + legend: { + type: 'category', + labels: ['Category 2'], + colors: ['#faf'] + } + } + ]; + return <Template layers={layers} />; +}; + +const LegendMultiTemplateCollapsed = () => { + const [collapsed, setCollapsed] = useState(true); + + const layers = [ + { + id: 0, + title: 'Layer 1', + visible: true, + legend: { + type: 'category', + labels: ['Category 1'], + colors: ['#faa'] + } + }, + { + id: 1, + title: 'Layer 2', + visible: false, + collapsible: false, + legend: { + type: 'category', + labels: ['Category 2'], + colors: ['#faf'] + } + } + ]; + + return ( + <Template + layers={layers} + collapsed={collapsed} + onChangeCollapsed={({ collapsed }) => setCollapsed(collapsed)} + /> + ); +}; + +const categoryLegend = { + type: 'category', + note: 'lorem', + colors: ['#000', '#00F', '#0F0'], + labels: ['Category 1', 'Category 2', 'Category 3'] +}; + +const LegendCategoriesTemplate = () => { + const layers = [ + { + id: 0, + title: 'Category Layer', + visible: true, + legend: categoryLegend + } + ]; + return <Template layers={layers} />; +}; + +const LegendCategoriesStrokeTemplate = () => { + const layers = [ + { + id: 0, + title: 'Category Layer as stroke', + visible: true, + legend: { + ...categoryLegend, + isStrokeColor: true + } + } + ]; + return <Template layers={layers} />; +}; + +const ICON = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICAgIDxkZWZzPgogICAgICAgIDxmaWx0ZXIgaWQ9InFsbTY3aXI1MWEiPgogICAgICAgICAgICA8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlR3JhcGhpYyIgdmFsdWVzPSIwIDAgMCAwIDAuMTcyNTQ5IDAgMCAwIDAgMC4xODgyMzUgMCAwIDAgMCAwLjE5NjA3OCAwIDAgMCAxLjAwMDAwMCAwIi8+CiAgICAgICAgPC9maWx0ZXI+CiAgICA8L2RlZnM+CiAgICA8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnPgogICAgICAgICAgICA8ZyBmaWx0ZXI9InVybCgjcWxtNjdpcjUxYSkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yNDIgLTQ2NCkgdHJhbnNsYXRlKDIyMiA0NDgpIj4KICAgICAgICAgICAgICAgIDxnIGZpbGw9IiMyQzMwMzIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIwIDE2KSI+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTEyIDBjNi42MjcgMCAxMiA1LjM3MyAxMiAxMnMtNS4zNzMgMTItMTIgMTJTMCAxOC42MjcgMCAxMiA1LjM3MyAwIDEyIDB6IiBvcGFjaXR5PSIuMiIvPgogICAgICAgICAgICAgICAgICAgIDxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjQiLz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+Cg==`; +const LegendIconTemplate = () => { + const layers = [ + { + id: 0, + title: 'Icon Layer', + visible: true, + legend: { + type: 'icon', + labels: ['Icon 1', 'Icon 2', 'Icon 3'], + icons: [ICON, ICON, ICON] + } + } + ]; + return <Template layers={layers} />; +}; + +const LegendRampTemplate = () => { + const layers = [ + { + id: 0, + title: 'Ramp Discontinuous', + visible: true, + legend: { + type: 'bins', + colors: ['#000', '#00F', '#0F0', '#F00'], + labels: [100, 200, 300], + showMinMax: true + } + }, + { + id: 1, + title: 'Ramp Continuous', + visible: true, + legend: { + type: 'continuous_ramp', + colors: ['#000', '#00F', '#0F0', '#F00'], + labels: [100, 200, 300] + } + } + ]; + + return <Template collapsed={false} layers={layers} />; +}; + +const LegendProportionTemplate = () => { + const layers = [ + { + id: 0, + title: 'Proportion Layer', + visible: true, + legend: { + type: 'proportion', + labels: [100, 500], + showMinMax: true + } + } + ]; + return <Template layers={layers} />; +}; + +const LegendCustomTemplate = () => { + const layers = [ + { + id: 0, + title: 'Single Layer', + visible: true, + legend: { + type: 'custom' + } + } + ]; + const customLegendTypes = { + custom: () => <div>Custom Legend</div> + }; + return <Template layers={layers} customLegendTypes={customLegendTypes} />; +}; + +const LegendNoChildrenTemplate = () => { + const layers = [ + { + id: 0, + title: 'Single Layer', + visible: true + } + ]; + return <Template layers={layers} />; +}; + +export const SingleLayer = LegendTemplate.bind({}); +export const MultiLayer = LegendMultiTemplate.bind({}); +export const MultiLayerCollapsed = LegendMultiTemplateCollapsed.bind({}); +export const NotFound = LegendNotFoundTemplate.bind({}); +export const LegendWithoutOpacityControl = LegendWithoutOpacityTemplate.bind({}); +export const Categories = LegendCategoriesTemplate.bind({}); +export const CategoriesAsStroke = LegendCategoriesStrokeTemplate.bind({}); +export const Icon = LegendIconTemplate.bind({}); +export const Ramp = LegendRampTemplate.bind({}); +export const Proportion = LegendProportionTemplate.bind({}); +export const Custom = LegendCustomTemplate.bind({}); +export const NoChildren = LegendNoChildrenTemplate.bind({}); diff --git a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendCategories.stories.js b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendCategories.stories.js index eaaafa55d..052b4d994 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendCategories.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendCategories.stories.js @@ -1,5 +1,5 @@ import React from 'react'; -import LegendCategories from '../../../../src/widgets/legend/LegendCategories'; +import LegendCategories from '../../../../src/widgets/new-legend/legend-types/LegendCategories'; const DEFAULT_LEGEND = { legend: { diff --git a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendIcon.stories.js b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendIcon.stories.js index a894ec4f7..87d865364 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendIcon.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendIcon.stories.js @@ -1,5 +1,5 @@ import React from 'react'; -import LegendIcon from '../../../../src/widgets/legend/LegendIcon'; +import LegendIcon from '../../../../src/widgets/new-legend/legend-types/LegendIcon'; const ICON = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICAgIDxkZWZzPgogICAgICAgIDxmaWx0ZXIgaWQ9InFsbTY3aXI1MWEiPgogICAgICAgICAgICA8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlR3JhcGhpYyIgdmFsdWVzPSIwIDAgMCAwIDAuMTcyNTQ5IDAgMCAwIDAgMC4xODgyMzUgMCAwIDAgMCAwLjE5NjA3OCAwIDAgMCAxLjAwMDAwMCAwIi8+CiAgICAgICAgPC9maWx0ZXI+CiAgICA8L2RlZnM+CiAgICA8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnPgogICAgICAgICAgICA8ZyBmaWx0ZXI9InVybCgjcWxtNjdpcjUxYSkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yNDIgLTQ2NCkgdHJhbnNsYXRlKDIyMiA0NDgpIj4KICAgICAgICAgICAgICAgIDxnIGZpbGw9IiMyQzMwMzIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIwIDE2KSI+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTEyIDBjNi42MjcgMCAxMiA1LjM3MyAxMiAxMnMtNS4zNzMgMTItMTIgMTJTMCAxOC42MjcgMCAxMiA1LjM3MyAwIDEyIDB6IiBvcGFjaXR5PSIuMiIvPgogICAgICAgICAgICAgICAgICAgIDxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjQiLz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+Cg==`; diff --git a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendProportion.stories.js b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendProportion.stories.js index 57057bab9..c3adc085f 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendProportion.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendProportion.stories.js @@ -1,5 +1,5 @@ import React from 'react'; -import LegendProportion from '../../../../src/widgets/legend/LegendProportion'; +import LegendProportion from '../../../../src/widgets/new-legend/legend-types/LegendProportion'; import { IntlProvider } from 'react-intl'; const DEFAULT_LEGEND = { diff --git a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendRamp.stories.js b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendRamp.stories.js index 234ad5777..b95537f4b 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendRamp.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendRamp.stories.js @@ -1,5 +1,5 @@ import React from 'react'; -import LegendRamp from '../../../../src/widgets/legend/LegendRamp'; +import LegendRamp from '../../../../src/widgets/new-legend/legend-types/LegendRamp'; const DEFAULT_LEGEND = { legend: { diff --git a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js index e27db194e..6318efd7f 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js +++ b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js @@ -1,4 +1,4 @@ -import { LEGEND_TYPES } from '@carto/react-ui'; +import LEGEND_TYPES from '../../../src/widgets/new-legend/legend-types/LegendTypes'; export const fixtures = [ { diff --git a/packages/react-widgets/src/widgets/LegendWidget.js b/packages/react-widgets/src/widgets/LegendWidget.js index 48797b9ba..31fbc676f 100644 --- a/packages/react-widgets/src/widgets/LegendWidget.js +++ b/packages/react-widgets/src/widgets/LegendWidget.js @@ -4,24 +4,18 @@ import { LegendWidgetUI } from '@carto/react-ui'; import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import sortLayers from './utils/sortLayers'; +import { useMediaQuery } from '@mui/material'; /** * Renders a <LegendWidget /> component * @param {object} props * @param {string} [props.title] - Title of the widget. - * @param {Object.<string, Function>} [props.customLayerOptions] - Allow to add custom controls to a legend item to tweak the associated layer. * @param {Object.<string, Function>} [props.customLegendTypes] - Allow to customise by default legend types that can be rendered. * @param {boolean} [props.initialCollapsed] - Define initial collapsed value. false by default. * @param {string[]} [props.layerOrder] - Array of layer identifiers. Defines the order of layer legends. [] by default. - + * @returns {React.ReactNode} */ -function LegendWidget({ - customLayerOptions, - customLegendTypes, - initialCollapsed, - layerOrder = [], - title -}) { +function LegendWidget({ customLegendTypes, initialCollapsed, layerOrder = [], title }) { const dispatch = useDispatch(); const layers = useSelector((state) => sortLayers( @@ -30,6 +24,8 @@ function LegendWidget({ ) ); const [collapsed, setCollapsed] = useState(initialCollapsed); + const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm')); + const { zoom, maxZoom, minZoom } = useSelector((state) => state.carto.viewState); if (!layers.length) { return null; @@ -57,7 +53,31 @@ function LegendWidget({ dispatch( updateLayer({ id, - layerAttributes: { legend: { collapsed } } + layerAttributes: { collapsed } + }) + ); + }; + + const handleSelectionChange = ({ id, index, selection }) => { + const layer = layers.find((layer) => layer.id === id); + const isMultiple = Array.isArray(selection); + const legend = isMultiple ? layer.legend : layer.legend[index]; + const newLegend = { + ...legend, + select: { + ...legend.select, + value: selection + } + }; + + dispatch( + updateLayer({ + id, + layerAttributes: { + legend: isMultiple + ? layer.legend.map((l, i) => (i === index ? newLegend : l)) + : newLegend + } }) ); }; @@ -66,13 +86,17 @@ function LegendWidget({ <LegendWidgetUI title={title} customLegendTypes={customLegendTypes} - customLayerOptions={customLayerOptions} layers={layers} - onChangeVisibility={handleChangeVisibility} - onChangeOpacity={handleChangeOpacity} collapsed={collapsed} onChangeCollapsed={setCollapsed} onChangeLegendRowCollapsed={handleChangeLegendRowCollapsed} + onChangeVisibility={handleChangeVisibility} + onChangeOpacity={handleChangeOpacity} + onChangeSelection={handleSelectionChange} + isMobile={isMobile} + currentZoom={zoom} + maxZoom={maxZoom} + minZoom={minZoom} /> ); } From a9de216af8905e86ec2ced3ce054ee323c77b0e0 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Fri, 9 Feb 2024 17:54:00 +0100 Subject: [PATCH 41/66] rename storybook --- .../{NewLegendWidgetUI.stories.js => LegendWidgetUI.stories.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename packages/react-ui/storybook/stories/widgetsUI/{NewLegendWidgetUI.stories.js => LegendWidgetUI.stories.js} (99%) diff --git a/packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js b/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js similarity index 99% rename from packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js rename to packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js index 42651cbe9..8501eaf14 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/NewLegendWidgetUI.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js @@ -5,7 +5,7 @@ import { Box } from '@mui/material'; import { fixtures } from './legendFixtures'; const options = { - title: 'Widgets/NewLegendWidgetUI', + title: 'Widgets/LegendWidgetUI', component: LegendWidgetUI, argTypes: { collapsed: { From d80115f2bc02eefa6520ef10d8100c055d6c3d04 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Fri, 9 Feb 2024 17:55:31 +0100 Subject: [PATCH 42/66] rename all new-legend components to replace old legend folder --- .../__tests__/widgets/LegendWidgetUI.test.js | 2 +- .../widgets/legend/LegendCategories.test.js | 2 +- .../widgets/legend/LegendIcon.test.js | 2 +- .../widgets/legend/LegendProportion.test.js | 2 +- .../widgets/legend/LegendRamp.test.js | 2 +- packages/react-ui/src/index.d.ts | 12 +- packages/react-ui/src/index.js | 12 +- packages/react-ui/src/types.d.ts | 2 +- .../src/widgets/legend/LayerOptionWrapper.js | 14 - .../{new-legend => legend}/LegendLayer.js | 0 .../LegendLayerTitle.js | 0 .../LegendLayerVariable.js | 0 .../LegendOpacityControl.js | 0 .../LegendWidgetUI.d.ts | 0 .../src/widgets/legend/LegendWidgetUI.js | 400 ++++++++---------- .../LegendWidgetUI.styles.js | 0 .../src/widgets/legend/LegendWrapper.js | 204 --------- packages/react-ui/src/widgets/legend/Note.js | 16 - .../legend-types/LegendCategories.js | 0 .../legend-types/LegendIcon.js | 0 .../legend-types/LegendProportion.js | 0 .../legend-types/LegendRamp.js | 0 .../legend-types/LegendTypes.js | 0 .../src/widgets/new-legend/LegendWidgetUI.js | 198 --------- .../widgetsUI/LegendWidgetUI.stories.js | 2 +- .../legend/LegendCategories.stories.js | 2 +- .../widgetsUI/legend/LegendIcon.stories.js | 2 +- .../legend/LegendProportion.stories.js | 2 +- .../widgetsUI/legend/LegendRamp.stories.js | 2 +- .../stories/widgetsUI/legendFixtures.js | 2 +- 30 files changed, 198 insertions(+), 682 deletions(-) delete mode 100644 packages/react-ui/src/widgets/legend/LayerOptionWrapper.js rename packages/react-ui/src/widgets/{new-legend => legend}/LegendLayer.js (100%) rename packages/react-ui/src/widgets/{new-legend => legend}/LegendLayerTitle.js (100%) rename packages/react-ui/src/widgets/{new-legend => legend}/LegendLayerVariable.js (100%) rename packages/react-ui/src/widgets/{new-legend => legend}/LegendOpacityControl.js (100%) rename packages/react-ui/src/widgets/{new-legend => legend}/LegendWidgetUI.d.ts (100%) rename packages/react-ui/src/widgets/{new-legend => legend}/LegendWidgetUI.styles.js (100%) delete mode 100644 packages/react-ui/src/widgets/legend/LegendWrapper.js delete mode 100644 packages/react-ui/src/widgets/legend/Note.js rename packages/react-ui/src/widgets/{new-legend => legend}/legend-types/LegendCategories.js (100%) rename packages/react-ui/src/widgets/{new-legend => legend}/legend-types/LegendIcon.js (100%) rename packages/react-ui/src/widgets/{new-legend => legend}/legend-types/LegendProportion.js (100%) rename packages/react-ui/src/widgets/{new-legend => legend}/legend-types/LegendRamp.js (100%) rename packages/react-ui/src/widgets/{new-legend => legend}/legend-types/LegendTypes.js (100%) delete mode 100644 packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js diff --git a/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js b/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js index f091b0520..9b4214fed 100644 --- a/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js +++ b/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import LegendWidgetUI from '../../src/widgets/new-legend/LegendWidgetUI'; +import LegendWidgetUI from '../../src/widgets/legend/LegendWidgetUI'; import { fireEvent, render, screen } from '../widgets/utils/testUtils'; const MY_CUSTOM_LEGEND_KEY = 'my-custom-legend'; diff --git a/packages/react-ui/__tests__/widgets/legend/LegendCategories.test.js b/packages/react-ui/__tests__/widgets/legend/LegendCategories.test.js index 26f21cef2..250570fee 100644 --- a/packages/react-ui/__tests__/widgets/legend/LegendCategories.test.js +++ b/packages/react-ui/__tests__/widgets/legend/LegendCategories.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { render, screen } from '../utils/testUtils'; -import LegendCategories from '../../../src/widgets/new-legend/legend-types/LegendCategories'; +import LegendCategories from '../../../src/widgets/legend/legend-types/LegendCategories'; import { getPalette } from '../../../src/utils/palette'; import { hexToRgb } from '@mui/material'; diff --git a/packages/react-ui/__tests__/widgets/legend/LegendIcon.test.js b/packages/react-ui/__tests__/widgets/legend/LegendIcon.test.js index 54d97aaa4..9ad8b5187 100644 --- a/packages/react-ui/__tests__/widgets/legend/LegendIcon.test.js +++ b/packages/react-ui/__tests__/widgets/legend/LegendIcon.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { render, screen } from '../../widgets/utils/testUtils'; -import LegendIcon from '../../../src/widgets/new-legend/legend-types/LegendIcon'; +import LegendIcon from '../../../src/widgets/legend/legend-types/LegendIcon'; const ICON = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICAgIDxkZWZzPgogICAgICAgIDxmaWx0ZXIgaWQ9InFsbTY3aXI1MWEiPgogICAgICAgICAgICA8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlR3JhcGhpYyIgdmFsdWVzPSIwIDAgMCAwIDAuMTcyNTQ5IDAgMCAwIDAgMC4xODgyMzUgMCAwIDAgMCAwLjE5NjA3OCAwIDAgMCAxLjAwMDAwMCAwIi8+CiAgICAgICAgPC9maWx0ZXI+CiAgICA8L2RlZnM+CiAgICA8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnPgogICAgICAgICAgICA8ZyBmaWx0ZXI9InVybCgjcWxtNjdpcjUxYSkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yNDIgLTQ2NCkgdHJhbnNsYXRlKDIyMiA0NDgpIj4KICAgICAgICAgICAgICAgIDxnIGZpbGw9IiMyQzMwMzIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIwIDE2KSI+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTEyIDBjNi42MjcgMCAxMiA1LjM3MyAxMiAxMnMtNS4zNzMgMTItMTIgMTJTMCAxOC42MjcgMCAxMiA1LjM3MyAwIDEyIDB6IiBvcGFjaXR5PSIuMiIvPgogICAgICAgICAgICAgICAgICAgIDxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjQiLz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+Cg==`; const ICON_2 = `data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD//gA7Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2NjIpLCBxdWFsaXR5ID0gNzAK/9sAQwAKBwcIBwYKCAgICwoKCw4YEA4NDQ4dFRYRGCMfJSQiHyIhJis3LyYpNCkhIjBBMTQ5Oz4+PiUuRElDPEg3PT47/9sAQwEKCwsODQ4cEBAcOygiKDs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7/8AAEQgCAAIAAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A9gooopDCiiigCWiiikAUUUUAR0UUUigooooAfRRRQIKKKKAFooopiCiiigBaKKKBBRRRQA2iiirJCiiigCOiiimZhRRRQAtFFFcRqFFFFAC0UUV2lhRRRQA6iiikQFFFFAEdFFFBgFFFFAD6KKKyNgooooAKKKK2NAooooAWiiikIKKKKAG0UUUxhRRRQA2iiioEFFFFAEtFFFIAooooAjooopFBRRRQA+iiigQUUUUALRRRTEFFFFAC0UUUCCiiigBtFFFWSFFFFAEdFFFMzCiiigBaKKK4jUKKKKAFooortLCiiigB1FFFIgKKKKAI6KKKDAKKKKAH0UUVkbBRRRQAUUUVsaBRRRQAtFFFIQUUUUANooopjCiiigCSiiisyQooooAbRRRVkhRRRQAtFFFABRRRQBBRRRVgFFFFAE9FFFQWFFFFAFSiiisTUKKKKAEooorAsKKKKAGUUUViUFFFFAFeiiitjYKKKKAJaKKK6jkCiiigDRoooqzMKKKKAKtFFFQaBRRRQBcooopmIUUUUAV6KKKg0CiiigCeiiiqMwooooAiooorU5QooooAmooorI6gooooAbRRRVkhRRRQAtFFFABRRRQBBRRRVgFFFFAE9FFFQWFFFFAFSiiisTUKKKKAEooorAsKKKKAGUUUViUFFFFAFeiiitjYKKKKAJaKKK6jkCiiigDRoooqzMKKKKAKtFFFQaBRRRQBcooopmIUUUUAV6KKKg0CiiigCeiiiqMwooooAiooorU5QooooAdRRRSNAooooAWiiigYUUUUANooorA3CiiigBKKKK5igooooA4uiiivNPRCiiigDNooorzzqCiiigChRRRXCdAUUUUAOoooqCwooooAWiiigzCiiigBaKKKRyhRRRQBPRRRXWAUUUUAdBRRRXqkhRRRQB01FFFeweWFFFFAE1FFFdhmFFFFAEdFFFUaBRRRQBJRRRUkBRRRQAUUUUHMFFFFAC0UUUDCiiigBtFFFYG4UUUUAJRRRXMUFFFFAHF0UUV5p6IUUUUAZtFFFeedQUUUUAUKKKK4ToCiiigB1FFFQWFFFFAC0UUUGYUUUUALRRRSOUKKKKAJ6KKK6wCiiigDoKKKK9UkKKKKAOmooor2DywooooAmooorsMwooooAjoooqjQKKKKAJKKKKkgKKKKACiiigQUUUUAFFFFcwBRRRQBVooorzDoCiiigDmqKKK809AKKKKAIaKKKkoKKKKALlFFFdhAUUUUAaNFFFegYhRRRQBo0UUV6BxhRRRQBHRRRWBkFFFFAEdFFFcB1hRRRQBz1FFFcp1hRRRQBlUUUVwnQFFFFAGfRRRWYwooooA72iiivoTyQooooA2KKKK9U80KKKKAI6KKK4SwooooAkoooruNgooooAKKKK5gCiiigCrRRRXmHQFFFFAHNUUUV5p6AUUUUAQ0UUVJQUUUUAXKKKK7CAooooA0aKKK9AxCiiigDRooor0DjCiiigCOiiisDIKKKKAI6KKK4DrCiiigDnqKKK5TrCiiigDKooorhOgKKKKAM+iiisxhRRRQB3tFFFfQnkhRRRQBsUUUV6p5oUUUUAR0UUVwlhRRRQBPRRRXYWFFFFACUUUVJYUUUUAcbRRRQbhRRRQBxVFFFIoKKKKAO4ooopiCiiigDrqKKKDnCiiigBKKKKsxCiiigBlFFFMxCiiigCSiiig7AooooAbRRRQMKKKKAFooooEFFFFAGPRRRXgncFFFFAHFUUUV4p7QUUUUAZ1FFFamYUUUUAeg0UUV7Z44UUUUAdBRRRXqHEFFFFABRRRVEhRRRQAlFFFSWFFFFAHG0UUUG4UUUUAcVRRRSKCiiigDuKKKKYgooooA66iiig5wooooASiiirMQooooAZRRRTMQooooAkooooOwKKKKAG0UUUDCiiigBaKKKBBRRRQBj0UUV4J3BRRRQBxVFFFeKe0FFFFAGdRRRWpmFFFFAHoNFFFe2eOFFFFAHQUUUV6hxBRRRQA2iiitDIKKKKAPHaKKKzJCiiigDyKiiipAKKKKAIaKKKosKKKKAGUUUUxhRRRQB3tFFFUMKKKKAPoqiiimWFFFFABRRRQAUUUUAPooopGYUUUUAR0UUVJqFFFFAHEUUUV4R6YUUUUAZ9FFFeeegFFFFAEVFFFZm4UUUUAZ1FFFcpqFFFFAF6iiitTmCiiigD0yiiivuz5sKKKKAJaKKKk5gooooA8dooorMkKKKKAPIqKKKkAooooAhoooqiwooooAZRRRTGFFFFAHe0UUVQwooooA+iqKKKZYUUUUAFFFFABRRRQA+iiikZhRRRQBHRRRUmoUUUUAcRRRRXhHphRRRQBn0UUV556AUUUUARUUUVmbhRRRQBnUUUVymoUUUUAXqKKK1OYKKKKAPTKKKK+7PmwooooA46iiisTUKKKKAPnCiiiqICiiigD2CiiiqMwooooA9hoooqiAooooA5CiiimSFFFFAHzTRRRUjCiiigD1CiiigoKKKKAPeaKKKQwooooAKKKK5yAooooA5OiiivAPTCiiigDkqKKK4jsCiiigDvKKKK9484KKKKAN2iiivSPPCiiigDJooorwD0gooooA8xooorzz0wooooA9Cooor2znCiiigDLooor1DzwooooA+cKKKKogKKKKAPYKKKKozCiiigD2GiiiqICiiigDkKKKKZIUUUUAfNNFFFSMKKKKAPUKKKKCgooooA95ooopDCiiigAooornICiiigDk6KKK8A9MKKKKAOSoooriOwKKKKAO8ooor3jzgooooA3aKKK9I88KKKKAMmiiivAPSCiiigDzGiiivPPTCiiigD0KiiivbOcKKKKAPCqKKK9U8AKKKKAMeiiimUFFFFAH1lRRRQUFFFFADaKKKZYUUUUALRRRSEFFFFAHjFFFFQAUUUUAeO0UUUyAooooA+x6KKKzGFFFFACUUUUywooooA8tooor5A+nCiiigDpKKKK7DAKKKKAOwooor6k+aCiiigBlFFFMyCiiigBtFFFcB2hRRRQBxFFFFeaegFFFFACUUUV5hYUUUUAeSUUUV9ieEFFFFAGPRRRTKCiiigD6yooooKCiiigBtFFFMsKKKKAFooopCCiiigDxiiiioAKKKKAPHaKKKZAUUUUAfY9FFFZjCiiigBKKKKZYUUUUAeW0UUV8gfThRRRQB0lFFFdhgFFFFAHYUUUV9SfNBRRRQAyiiimZBRRRQA2iiiuA7QooooA4iiiivNPQCiiigBKKKK8wsKKKKAPKaKKK+yPHCiiigDAooooAKKKKAPrOiiigAooooAWiiigAooooAKKKKACiiigDwWiiigAooooA8xooooAKKKKAPsaiiigAooooASiiisygooooA8rooor4g9wKKKKAOlooorrJCiiigDsKKKK+xPCCiiigBtFFFMyCiiigCCiiiuM7gooooA4aiiivKPaCiiigCxRRRXCc4UUUUAeTUUUV9aeKFFFFAGBRRRQAUUUUAfWdFFFABRRRQAtFFFABRRRQAUUUUAFFFFAHgtFFFABRRRQB5jRRRQAUUUUAfY1FFFABRRRQAlFFFZlBRRRQB5XRRRXxB7gUUUUAdLRRRXWSFFFFAHYUUUV9ieEFFFFADaKKKZkFFFFAEFFFFcZ3BRRRQBw1FFFeUe0FFFFAFiiiiuE5wooooAo0UUV9aeKFFFFAHgNFFFABRRRQB6tRRRQAUUUUAen0UUUAFFFFAHA0UUUAFFFFAHjFFFFABRRRQB67RRRQAUUUUAe10UUUAFFFFADaKKKZAUUUUAcxRRRXinshRRRQBxFFFFeWdoUUUUAd1RRRXonEFFFFAGvRRRXpHGFFFFAGLRRRXjHqBRRRQBwlFFFchYUUUUAel0UUV7RwBRRRQBx1FFFeoeaFFFFAHgNFFFABRRRQB6tRRRQAUUUUAen0UUUAFFFFAHA0UUUAFFFFAHjFFFFABRRRQB67RRRQAUUUUAe10UUUAFFFFADaKKKZAUUUUAcxRRRXinshRRRQBxFFFFeWdoUUUUAd1RRRXonEFFFFAGvRRRXpHGFFFFAGLRRRXjHqBRRRQBwlFFFchYUUUUAel0UUV7RwBRRRQBq0UUV6h5oUUUUAfJFFFFABRRRQBnUUUUAFFFFAGhRRRQAUUUUAR0UUUAFFFFAHplFFFABRRRQB7pRRRQAUUUUANooooAKKKKAIaKKKyOUKKKKAH0UUVkdoUUUUAc1RRRXzx7QUUUUAc7RRRXEMKKKKALdFFFdBIUUUUAZVFFFcIwooooAfRRRTICiiigDraKKK+oOcKKKKAOiooor1TzQooooA+SKKKKACiiigDOooooAKKKKANCiiigAooooAjooooAKKKKAPTKKKKACiiigD3SiiigAooooAbRRRQAUUUUAQ0UUVkcoUUUUAPooorI7QooooA5qiiivnj2gooooA52iiiuIYUUUUAW6KKK6CQooooAyqKKK4RhRRRQA+iiimQFFFFAHW0UUV9Qc4UUUUAdJRRRXqnmhRRRQBztFFFABRRRQB8r0UUUAFFFFABRRRQAUUUUAfUlFFFABRRRQB1FFFFABRRRQBXooooAKKKKAJqKKKACiiigBtFFFUSFFFFABRRRQaBRRRQBFRRRVmgUUUUAZVFFFcZ0BRRRQBxlFFFcZAUUUUAaVFFFIkKKKKANuiiiuoYUUUUAblFFFdZkFFFFAE1FFFIyCiiigDnaKKKACiiigD5XooooAKKKKACiiigAooooA+pKKKKACiiigDqKKKKACiiigCvRRRQAUUUUATUUUUAFFFFADaKKKokKKKKACiiig0CiiigCKiiirNAooooAyqKKK4zoCiiigDjKKKK4yAooooA0qKKKRIUUUUAbdFFFdQwooooA3KKKK6zIKKKKAJKKKKRkFFFFAElFFFABRRRQBytFFFABRRRQB4VRRRQAUUUUAclRRRQAUUUUAdrRRRQAUUUUAd5RRRQAUUUUAdHRRRQAUUUUAV6KKKosKKKKAMKiiisjMKKKKAOJooorrAKKKKAOIooopAFFFFAGtRRRXGIKKKKAPf6KKK6TMKKKKAOzoooqDUKKKKAEooorlLCiiigBKKKK6SQooooAkooooAKKKKAOVooooAKKKKAPCqKKKACiiigDkqKKKACiiigDtaKKKACiiigDvKKKKACiiigDo6KKKACiiigCvRRRVFhRRRQBhUUUVkZhRRRQBxNFFFdYBRRRQBxFFFFIAooooA1qKKK4xBRRRQB7/RRRXSZhRRRQB2dFFFQahRRRQAlFFFcpYUUUUAPooorpJCiiigCtRRRQAUUUUAWaKKKACiiigCvRRRQAUUUUAeV0UUUAFFFFAHl9FFFABRRRQBz9FFFABRRRQAUUUUAFFFFAElFFFQUFFFFAEdFFFUIKKKKAOgooopDCiiigD0miiigsKKKKAPYaKKKszCiiigBKKKKZAUUUUAJRRRQIKKKKAG0UUVzjCiiigCaiiitigooooArUUUUAFFFFAFmiiigAooooAr0UUUAFFFFAHldFFFABRRRQB5fRRRQAUUUUAc/RRRQAUUUUAFFFFABRRRQBJRRRUFBRRRQBHRRRVCCiiigDoKKKKQwooooA9JooooLCiiigD2GiiirMwooooASiiimQFFFFACUUUUCCiiigBtFFFc4wooooAlooorYoKKKKAFooooAKKKKAI6KKKACiiigCSiiigAooooAKKKKACiiigCGiiigAooooAbRRRQAUUUUAZdFFFABRRRQA+iiishBRRRQBq0UUVuWFFFFAEVFFFYlBRRRQBNRRRWJIUUUUAFFFFdYBRRRQAlFFFMAooooAWiiiggKKKKACiiipJCiiigB1FFFBYUUUUALRRRQAUUUUAR0UUUAFFFFAElFFFABRRRQAUUUUAFFFFAENFFFABRRRQA2iiigAooooAy6KKKACiiigB9FFFZCCiiigDVooorcsKKKKAIqKKKxKCiiigCaiiisSQooooAKKKK6wCiiigBKKKKYBRRRQAtFFFBAUUUUAFFFFSSFFFFABRRRQWFFFFAD6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigBKKKKACiiigBaKKKACiiigBtFFFABRRRQAUUUUyAooooAbRRRTNAooooAfRRRUiCiiigCOiiipMgooooAfRRRTLCiiigBKKKKZoFFFFAC0UUUwCiiigBtFFFQZhRRRQAtFFFUUFFFFAD6KKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigBKKKKACiiigBaKKKACiiigBtFFFABRRRQAUUUUyAooooAbRRRTNAooooAfRRRUiCiiigCOiiipMgooooAfRRRTLCiiigBKKKKZoFFFFAC0UUUwCiiigBtFFFQZhRRRQB//Z`; diff --git a/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js b/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js index 18cb4f811..72f81d297 100644 --- a/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js +++ b/packages/react-ui/__tests__/widgets/legend/LegendProportion.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { render, screen } from '../../widgets/utils/testUtils'; -import LegendProportion from '../../../src/widgets/new-legend/legend-types/LegendProportion'; +import LegendProportion from '../../../src/widgets/legend/legend-types/LegendProportion'; import { IntlProvider } from 'react-intl'; const DEFAULT_LEGEND = { diff --git a/packages/react-ui/__tests__/widgets/legend/LegendRamp.test.js b/packages/react-ui/__tests__/widgets/legend/LegendRamp.test.js index ef9b4c325..89831bb15 100644 --- a/packages/react-ui/__tests__/widgets/legend/LegendRamp.test.js +++ b/packages/react-ui/__tests__/widgets/legend/LegendRamp.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { render, screen } from '../../widgets/utils/testUtils'; -import LegendRamp from '../../../src/widgets/new-legend/legend-types/LegendRamp'; +import LegendRamp from '../../../src/widgets/legend/legend-types/LegendRamp'; import { getPalette } from '../../../src/utils/palette'; import { hexToRgb } from '@mui/material'; diff --git a/packages/react-ui/src/index.d.ts b/packages/react-ui/src/index.d.ts index a4606baa3..ed9d8cc64 100644 --- a/packages/react-ui/src/index.d.ts +++ b/packages/react-ui/src/index.d.ts @@ -9,12 +9,12 @@ import FormulaWidgetUI from './widgets/FormulaWidgetUI/FormulaWidgetUI'; import BarWidgetUI from './widgets/BarWidgetUI/BarWidgetUI'; import HistogramWidgetUI from './widgets/HistogramWidgetUI/HistogramWidgetUI'; import PieWidgetUI from './widgets/PieWidgetUI/PieWidgetUI'; -import LegendWidgetUI from './widgets/new-legend/LegendWidgetUI'; -import LEGEND_TYPES from './widgets/new-legend/legend-types/LegendTypes' -import LegendCategories from './widgets/new-legend/legend-types/LegendCategories'; -import LegendIcon from './widgets/new-legend/legend-types/LegendIcon'; -import LegendProportion from './widgets/new-legend/legend-types/LegendProportion'; -import LegendRamp from './widgets/new-legend/legend-types/LegendRamp'; +import LegendWidgetUI from './widgets/legend/LegendWidgetUI'; +import LEGEND_TYPES from './widgets/legend/legend-types/LegendTypes' +import LegendCategories from './widgets/legend/legend-types/LegendCategories'; +import LegendIcon from './widgets/legend/legend-types/LegendIcon'; +import LegendProportion from './widgets/legend/legend-types/LegendProportion'; +import LegendRamp from './widgets/legend/legend-types/LegendRamp'; import ScatterPlotWidgetUI from './widgets/ScatterPlotWidgetUI/ScatterPlotWidgetUI'; import TimeSeriesWidgetUI from './widgets/TimeSeriesWidgetUI/TimeSeriesWidgetUI'; import { diff --git a/packages/react-ui/src/index.js b/packages/react-ui/src/index.js index 2feed3764..06f591a7a 100644 --- a/packages/react-ui/src/index.js +++ b/packages/react-ui/src/index.js @@ -5,12 +5,12 @@ import FormulaWidgetUI from './widgets/FormulaWidgetUI/FormulaWidgetUI'; import BarWidgetUI from './widgets/BarWidgetUI/BarWidgetUI'; import HistogramWidgetUI from './widgets/HistogramWidgetUI/HistogramWidgetUI'; import PieWidgetUI from './widgets/PieWidgetUI/PieWidgetUI'; -import LegendWidgetUI from './widgets/new-legend/LegendWidgetUI'; -import LEGEND_TYPES from './widgets/new-legend/legend-types/LegendTypes'; -import LegendCategories from './widgets/new-legend/legend-types/LegendCategories'; -import LegendIcon from './widgets/new-legend/legend-types/LegendIcon'; -import LegendProportion from './widgets/new-legend/legend-types/LegendProportion'; -import LegendRamp from './widgets/new-legend/legend-types/LegendRamp'; +import LegendWidgetUI from './widgets/legend/LegendWidgetUI'; +import LEGEND_TYPES from './widgets/legend/legend-types/LegendTypes'; +import LegendCategories from './widgets/legend/legend-types/LegendCategories'; +import LegendIcon from './widgets/legend/legend-types/LegendIcon'; +import LegendProportion from './widgets/legend/legend-types/LegendProportion'; +import LegendRamp from './widgets/legend/legend-types/LegendRamp'; import ScatterPlotWidgetUI from './widgets/ScatterPlotWidgetUI/ScatterPlotWidgetUI'; import TimeSeriesWidgetUI from './widgets/TimeSeriesWidgetUI/TimeSeriesWidgetUI'; import FeatureSelectionWidgetUI from './widgets/FeatureSelectionWidgetUI/FeatureSelectionWidgetUI'; diff --git a/packages/react-ui/src/types.d.ts b/packages/react-ui/src/types.d.ts index 4ff79b0ac..084aeca74 100644 --- a/packages/react-ui/src/types.d.ts +++ b/packages/react-ui/src/types.d.ts @@ -21,7 +21,7 @@ export { LegendLayerVariableBase, LegendColors, LegendNumericLabels -} from './widgets/new-legend/LegendWidgetUI' +} from './widgets/legend/LegendWidgetUI' export type WrapperWidgetUI = { title: string; diff --git a/packages/react-ui/src/widgets/legend/LayerOptionWrapper.js b/packages/react-ui/src/widgets/legend/LayerOptionWrapper.js deleted file mode 100644 index d1378ae6d..000000000 --- a/packages/react-ui/src/widgets/legend/LayerOptionWrapper.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import { Box } from '@mui/material'; -import Typography from '../../components/atoms/Typography'; - -export default function LayerOptionWrapper({ label, children }) { - return ( - <Box px={2} py={1.5}> - <Typography variant='caption' color='textPrimary'> - {label} - </Typography> - <Box>{children}</Box> - </Box> - ); -} diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayer.js b/packages/react-ui/src/widgets/legend/LegendLayer.js similarity index 100% rename from packages/react-ui/src/widgets/new-legend/LegendLayer.js rename to packages/react-ui/src/widgets/legend/LegendLayer.js diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js b/packages/react-ui/src/widgets/legend/LegendLayerTitle.js similarity index 100% rename from packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js rename to packages/react-ui/src/widgets/legend/LegendLayerTitle.js diff --git a/packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js b/packages/react-ui/src/widgets/legend/LegendLayerVariable.js similarity index 100% rename from packages/react-ui/src/widgets/new-legend/LegendLayerVariable.js rename to packages/react-ui/src/widgets/legend/LegendLayerVariable.js diff --git a/packages/react-ui/src/widgets/new-legend/LegendOpacityControl.js b/packages/react-ui/src/widgets/legend/LegendOpacityControl.js similarity index 100% rename from packages/react-ui/src/widgets/new-legend/LegendOpacityControl.js rename to packages/react-ui/src/widgets/legend/LegendOpacityControl.js diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.d.ts b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts similarity index 100% rename from packages/react-ui/src/widgets/new-legend/LegendWidgetUI.d.ts rename to packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/legend/LegendWidgetUI.js index 3a7bc8247..6f53fb1fe 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.js @@ -1,250 +1,198 @@ -import React, { createRef, Fragment } from 'react'; -import { Box, Button, Collapse, Divider, Grid, styled, SvgIcon } from '@mui/material'; -import LegendWrapper from './LegendWrapper'; -import LegendCategories from '../new-legend/legend-types/LegendCategories'; -import LegendIcon from '../new-legend/legend-types/LegendIcon'; -import LegendRamp from '../new-legend/legend-types/LegendRamp'; -import LegendProportion from '../new-legend/legend-types/LegendProportion'; +import React from 'react'; import PropTypes from 'prop-types'; -import Typography from '../../components/atoms/Typography'; +import { + Box, + Collapse, + Drawer, + IconButton, + Paper, + Tooltip, + Typography +} from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import LayerIcon from '@mui/icons-material/LayersOutlined'; +import { LEGEND_WIDTH, styles } from './LegendWidgetUI.styles'; +import LegendLayer from './LegendLayer'; +import { useIntl } from 'react-intl'; +import useImperativeIntl from '../../hooks/useImperativeIntl'; + +const EMPTY_OBJ = {}; +const EMPTY_FN = () => {}; +const EMPTY_ARR = []; + +/** + * @param {object} props + * @param {Object.<string, import('./LegendWidgetUI').CustomLegendComponent>} [props.customLegendTypes] - Allow to customise by default legend types that can be rendered. + * @param {import('./LegendWidgetUI').LegendLayerData[]} [props.layers] - Array of layer objects from redux store. + * @param {boolean} [props.collapsed] - Collapsed state for whole legend widget. + * @param {(collapsed: boolean) => void} [props.onChangeCollapsed] - Callback function for collapsed state change. + * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} [props.onChangeLegendRowCollapsed] - Callback function for layer visibility change. + * @param {({ id, opacity }: { id: string, opacity: number }) => void} [props.onChangeOpacity] - Callback function for layer opacity change. + * @param {({ id, visible }: { id: string, visible: boolean }) => void} [props.onChangeVisibility] - Callback function for layer collapsed state change. + * @param {({ id, index, selection }: { id: string, index: number, selection: unknown }) => void} props.onChangeSelection - Callback function for layer variable selection change. + * @param {string} [props.title] - Title of the toggle button when widget is open. + * @param {'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'} [props.position] - Position of the widget. + * @param {number} [props.maxZoom] - Global maximum zoom level for the map. + * @param {number} [props.minZoom] - Global minimum zoom level for the map. + * @param {number} [props.currentZoom] - Current zoom level for the map. + * @param {boolean} [props.isMobile] - Whether the widget is displayed on a mobile device. + * @returns {React.ReactNode} + */ +function LegendWidgetUI({ + customLegendTypes = EMPTY_OBJ, + layers = EMPTY_ARR, + collapsed = false, + onChangeCollapsed = EMPTY_FN, + onChangeVisibility = EMPTY_FN, + onChangeOpacity = EMPTY_FN, + onChangeLegendRowCollapsed = EMPTY_FN, + onChangeSelection = EMPTY_FN, + title, + position = 'bottom-right', + maxZoom = 21, + minZoom = 0, + currentZoom, + isMobile +} = {}) { + const intl = useIntl(); + const intlConfig = useImperativeIntl(intl); + const isSingle = layers.length === 1; -const LayersIcon = () => ( - <SvgIcon width='24' height='24' viewBox='0 0 24 24'> - <defs> - <path - id='5chkhs3l0a' - d='M11.99 19.005l-7.37-5.73L3 14.535l9 7 9-7-1.63-1.27-7.38 5.74zm.01-2.54l7.36-5.73L21 9.465l-9-7-9 7 1.63 1.27 7.37 5.73zm0-11.47l5.74 4.47-5.74 4.47-5.74-4.47L12 4.995z' - /> - </defs> - <g transform='translate(-434 -298) translate(230 292) translate(204 6)'> - <mask id='z57f7rm2gb' fill='#fff'> - <use xlinkHref='#5chkhs3l0a' /> - </mask> - <g fill='#2C3032' mask='url(#z57f7rm2gb)'> - <path d='M0 0H24V24H0z' /> - </g> - </g> - </SvgIcon> -); + const rootSx = { + ...styles[position], + ...styles.root, + width: collapsed || isMobile ? undefined : LEGEND_WIDTH + }; -const LegendBox = styled(Box)(({ theme }) => ({ - minWidth: theme.spacing(30), - backgroundColor: theme.palette.background.paper, - boxShadow: theme.shadows[1], - borderRadius: theme.spacing(0.5) -})); + const legendToggleHeader = ( + <Box + sx={{ + ...styles.legendToggle, + ...(!collapsed && styles.legendToggleOpen) + }} + > + <Typography variant='caption' sx={{ flexGrow: 1 }}> + {title} + </Typography> + <Tooltip title={intlConfig.formatMessage({ id: 'c4r.widgets.legend.close' })}> + <IconButton size='small' onClick={() => onChangeCollapsed(true)}> + <CloseIcon /> + </IconButton> + </Tooltip> + </Box> + ); + const legendToggleButton = ( + <Tooltip title={intlConfig.formatMessage({ id: 'c4r.widgets.legend.open' })}> + <IconButton aria-label={title} onClick={() => onChangeCollapsed(false)}> + <LayerIcon /> + </IconButton> + </Tooltip> + ); -function LegendWidgetUI({ - customLegendTypes, - customLayerOptions, - layers = [], - collapsed, - onChangeCollapsed, - onChangeVisibility, - onChangeOpacity, - onChangeLegendRowCollapsed, - title -}) { - const isSingle = layers.length === 1; + if (isSingle) { + return ( + <Paper elevation={3} sx={rootSx}> + <Box style={styles.legendItemList}> + <LegendLayer + layer={layers[0]} + onChangeCollapsed={onChangeLegendRowCollapsed} + onChangeOpacity={onChangeOpacity} + onChangeVisibility={onChangeVisibility} + onChangeSelection={onChangeSelection} + maxZoom={maxZoom} + minZoom={minZoom} + currentZoom={currentZoom} + customLegendTypes={customLegendTypes} + /> + </Box> + </Paper> + ); + } return ( - <LegendBox> - <LegendContainer - isSingle={isSingle} - collapsed={collapsed} - onChangeCollapsed={onChangeCollapsed} - title={title} - > - <LegendRows - layers={layers} - customLegendTypes={customLegendTypes} - customLayerOptions={customLayerOptions} - onChangeVisibility={onChangeVisibility} - onChangeOpacity={onChangeOpacity} - onChangeCollapsed={onChangeLegendRowCollapsed} - /> - </LegendContainer> - </LegendBox> + <Paper elevation={3} sx={rootSx}> + {isMobile ? ( + <> + {legendToggleButton} + <Drawer + anchor='bottom' + open={!collapsed} + onClose={() => onChangeCollapsed(true)} + > + {legendToggleHeader} + <Box style={styles.legendItemList}> + {layers.map((l) => ( + <LegendLayer + key={l.id} + layer={l} + onChangeCollapsed={onChangeLegendRowCollapsed} + onChangeOpacity={onChangeOpacity} + onChangeVisibility={onChangeVisibility} + onChangeSelection={onChangeSelection} + maxZoom={maxZoom} + minZoom={minZoom} + currentZoom={currentZoom} + customLegendTypes={customLegendTypes} + /> + ))} + </Box> + </Drawer> + </> + ) : ( + <> + {collapsed ? legendToggleButton : legendToggleHeader} + <Box sx={{ ...styles.legendItemList, width: collapsed ? 0 : undefined }}> + <Collapse unmountOnExit in={!collapsed} timeout={500}> + {layers.map((l) => ( + <LegendLayer + key={l.id} + layer={l} + onChangeCollapsed={onChangeLegendRowCollapsed} + onChangeOpacity={onChangeOpacity} + onChangeVisibility={onChangeVisibility} + onChangeSelection={onChangeSelection} + maxZoom={maxZoom} + minZoom={minZoom} + currentZoom={currentZoom} + customLegendTypes={customLegendTypes} + /> + ))} + </Collapse> + </Box> + </> + )} + </Paper> ); } LegendWidgetUI.defaultProps = { - layers: [], - customLegendTypes: {}, - customLayerOptions: {}, + layers: EMPTY_ARR, + customLegendTypes: EMPTY_OBJ, collapsed: false, - title: 'Layers' + title: 'Layers', + position: 'bottom-right', + onChangeCollapsed: EMPTY_FN, + onChangeLegendRowCollapsed: EMPTY_FN, + onChangeVisibility: EMPTY_FN, + onChangeOpacity: EMPTY_FN, + onChangeSelection: EMPTY_FN }; LegendWidgetUI.propTypes = { customLegendTypes: PropTypes.objectOf(PropTypes.func), - customLayerOptions: PropTypes.objectOf(PropTypes.func), layers: PropTypes.array, collapsed: PropTypes.bool, onChangeCollapsed: PropTypes.func, onChangeLegendRowCollapsed: PropTypes.func, onChangeVisibility: PropTypes.func, onChangeOpacity: PropTypes.func, - title: PropTypes.string + onChangeSelection: PropTypes.func, + title: PropTypes.string, + position: PropTypes.oneOf(['top-left', 'top-right', 'bottom-left', 'bottom-right']), + maxZoom: PropTypes.number, + minZoom: PropTypes.number, + currentZoom: PropTypes.number, + isMobile: PropTypes.bool }; export default LegendWidgetUI; - -const Header = styled(Grid)(({ theme }) => ({ - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - height: theme.spacing(4.5) -})); - -const HeaderButton = styled(Button, { - shouldForwardProp: (prop) => prop !== 'collapsed' -})(({ theme, collapsed }) => ({ - flex: '1 1 auto', - justifyContent: 'space-between', - padding: theme.spacing(0.75, 1.25, 0.75, 3), - borderTop: collapsed ? 'none' : `1px solid ${theme.palette.divider}`, - cursor: 'pointer' -})); - -const CollapseWrapper = styled(Collapse)(() => ({ - '.MuiCollapse-wrapperInner': { - maxHeight: 'max(350px, 80vh)', - overflowY: 'auto', - overflowX: 'hidden' - } -})); - -function LegendContainer({ isSingle, children, collapsed, onChangeCollapsed, title }) { - const wrapper = createRef(); - - const handleExpandClick = () => { - if (onChangeCollapsed) onChangeCollapsed(!collapsed); - }; - - return isSingle ? ( - children - ) : ( - <> - <CollapseWrapper ref={wrapper} in={!collapsed} timeout='auto' unmountOnExit> - {children} - </CollapseWrapper> - <Header container> - <HeaderButton - collapsed={collapsed} - endIcon={<LayersIcon />} - onClick={handleExpandClick} - > - <Typography variant='subtitle1'>{title}</Typography> - </HeaderButton> - </Header> - </> - ); -} - -export const LEGEND_TYPES = { - CATEGORY: 'category', - ICON: 'icon', - CONTINUOUS_RAMP: 'continuous_ramp', - BINS: 'bins', - PROPORTION: 'proportion', - CUSTOM: 'custom' -}; - -const LEGEND_COMPONENT_BY_TYPE = { - [LEGEND_TYPES.CATEGORY]: LegendCategories, - [LEGEND_TYPES.ICON]: LegendIcon, - [LEGEND_TYPES.CONTINUOUS_RAMP]: (args) => <LegendRamp {...args} isContinuous={true} />, - [LEGEND_TYPES.BINS]: (args) => <LegendRamp {...args} isContinuous={false} />, - [LEGEND_TYPES.PROPORTION]: LegendProportion, - [LEGEND_TYPES.CUSTOM]: ({ legend }) => legend.children -}; - -function UnknownLayerOption({ optionKey }) { - return ( - <p> - Unknown layer option{' '} - <em> - <strong>{optionKey}</strong> - </em> - </p> - ); -} - -function LegendRows({ - layers = [], - customLegendTypes, - customLayerOptions, - onChangeVisibility, - onChangeOpacity, - onChangeCollapsed -}) { - const isSingle = layers.length === 1; - - return ( - <> - {layers.map((layer, index) => { - const { - id, - title, - switchable, - visible, - options = [], - showOpacityControl = false, - opacity = 1, - legend = {} - } = layer; - - const { - type = LEGEND_TYPES.CUSTOM, - collapsible = true, - collapsed = false, - note = '', - attr = '', - children - } = legend; - - const isLast = layers.length - 1 === index; - const LegendComponent = - LEGEND_COMPONENT_BY_TYPE[type] || customLegendTypes[type] || UnknownLegend; - const hasChildren = type === LEGEND_TYPES.CUSTOM ? !!children : !!LegendComponent; - - const layerOptions = options.map((key) => { - const Component = customLayerOptions[key] || UnknownLayerOption; - return <Component key={key} optionKey={key} layer={layer} />; - }); - - return ( - <Fragment key={id}> - <LegendWrapper - id={id} - title={title} - layerOptions={layerOptions} - hasChildren={hasChildren} - collapsible={collapsible} - collapsed={collapsed} - switchable={switchable} - visible={visible} - note={note} - attr={attr} - showOpacityControl={showOpacityControl} - opacity={opacity} - onChangeOpacity={onChangeOpacity} - onChangeVisibility={onChangeVisibility} - onChangeCollapsed={onChangeCollapsed} - > - <LegendComponent layer={layer} legend={legend} /> - </LegendWrapper> - {!isSingle && !isLast && <Divider />} - </Fragment> - ); - })} - </> - ); -} - -function UnknownLegend({ legend }) { - return ( - <Typography variant='body2'>{legend.type} is not a known legend type.</Typography> - ); -} diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js b/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js similarity index 100% rename from packages/react-ui/src/widgets/new-legend/LegendWidgetUI.styles.js rename to packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js diff --git a/packages/react-ui/src/widgets/legend/LegendWrapper.js b/packages/react-ui/src/widgets/legend/LegendWrapper.js deleted file mode 100644 index 766b28326..000000000 --- a/packages/react-ui/src/widgets/legend/LegendWrapper.js +++ /dev/null @@ -1,204 +0,0 @@ -import React, { createRef, useState } from 'react'; -import { ExpandLess, ExpandMore } from '@mui/icons-material'; -import { useIntl } from 'react-intl'; -import { - Box, - Button, - Collapse, - Grid, - Icon, - Switch, - ToggleButton, - Tooltip, - styled -} from '@mui/material'; -import LayerIcon from '../../assets/icons/LayerIcon'; -import Typography from '../../components/atoms/Typography'; -import OpacityControl from '../OpacityControl'; -import Note from './Note'; -import useImperativeIntl from '../../hooks/useImperativeIntl'; - -const Wrapper = styled(Box)(() => ({ - position: 'relative', - maxWidth: '100%', - padding: 0 -})); - -const LayerOptionsWrapper = styled(Box)(({ theme }) => ({ - backgroundColor: theme.palette.grey[50], - marginTop: theme.spacing(2) -})); - -function LegendWrapper({ - id, - title, - layerOptions, - switchable = true, - collapsible = true, - collapsed = false, - visible = true, - hasChildren = true, - note, - attr, - children, - showOpacityControl, - opacity, - onChangeOpacity, - onChangeVisibility, - onChangeCollapsed -}) { - const wrapper = createRef(); - const expanded = !collapsed; - const [isLayerOptionsExpanded, setIsLayerOptionsExpanded] = useState(false); - - const intl = useIntl(); - const intlConfig = useImperativeIntl(intl); - - const handleChangeOpacity = (newOpacity) => { - if (onChangeOpacity) onChangeOpacity({ id, opacity: newOpacity }); - }; - - const handleExpandClick = () => { - if (collapsible && onChangeCollapsed) - onChangeCollapsed({ id, collapsed: !collapsed }); - }; - - const handleChangeVisibility = () => { - if (onChangeVisibility) onChangeVisibility({ id, visible: !visible }); - }; - - const handleToggleLayerOptions = () => { - setIsLayerOptionsExpanded((oldState) => !oldState); - }; - - return ( - <Wrapper component='section' aria-label={title}> - <Header - title={title} - switchable={switchable} - visible={visible} - expanded={expanded} - collapsible={hasChildren && collapsible} - onExpandClick={handleExpandClick} - onChangeVisibility={handleChangeVisibility} - layerOptionsEnabled={showOpacityControl || layerOptions.length > 0} - onToggleLayerOptions={handleToggleLayerOptions} - isLayerOptionsExpanded={isLayerOptionsExpanded} - intl={intlConfig} - /> - {hasChildren && !!children && ( - <Collapse ref={wrapper} in={expanded} timeout='auto' unmountOnExit> - <Box pt={1} pl={3} pr={2} pb={2}> - <Grid container direction='column' spacing={1}> - {attr && ( - <Typography xs mb={1} variant='caption'> - {intlConfig.formatMessage({ id: 'c4r.widgets.legend.by' }, { attr })} - </Typography> - )} - {children} - <Collapse in={isLayerOptionsExpanded} timeout='auto' unmountOnExit> - <LayerOptionsWrapper> - {showOpacityControl && ( - <OpacityControl - opacity={opacity} - onChangeOpacity={handleChangeOpacity} - /> - )} - {layerOptions} - </LayerOptionsWrapper> - </Collapse> - <Note>{note}</Note> - </Grid> - </Box> - </Collapse> - )} - </Wrapper> - ); -} - -const GridHeader = styled(Grid)(({ theme }) => ({ - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - height: '60px', - padding: theme.spacing(1.25, 1.25, 1.25, 2.5) -})); - -const ButtonHeader = styled(Button, { - shouldForwardProp: (prop) => prop !== 'collapsible' -})(({ theme, collapsible }) => ({ - padding: 0, - flex: '1 1 auto', - justifyContent: 'flex-start', - cursor: collapsible ? 'pointer' : 'default', - '& .MuiButton-startIcon': { - marginRight: theme.spacing(1) - }, - '&:hover': { - background: 'none' - } -})); - -const ParentIcon = ({ theme }) => ({ - display: 'block', - fill: theme.palette.text.secondary -}); - -const MoreIconHeader = styled(ExpandMore)(({ theme }) => ParentIcon({ theme })); -const LessIconHeader = styled(ExpandLess)(({ theme }) => ParentIcon({ theme })); - -function Header({ - title, - switchable, - visible, - collapsible, - expanded, - onExpandClick, - onChangeVisibility, - layerOptionsEnabled, - onToggleLayerOptions, - isLayerOptionsExpanded, - intl -}) { - const ExpandIcon = expanded ? LessIconHeader : MoreIconHeader; - - return ( - <GridHeader container> - <ButtonHeader - collapsible={collapsible.toString()} - startIcon={ - collapsible && ( - <Icon> - <ExpandIcon /> - </Icon> - ) - } - onClick={onExpandClick} - > - <Typography variant='subtitle1'>{title}</Typography> - </ButtonHeader> - {!!layerOptionsEnabled && ( - <Tooltip title={intl.formatMessage({ id: 'c4r.widgets.legend.layerOptions' })}> - <ToggleButton - selected={isLayerOptionsExpanded} - onClick={onToggleLayerOptions} - value='check' - > - <LayerIcon /> - </ToggleButton> - </Tooltip> - )} - {switchable && ( - <Tooltip - title={intl.formatMessage({ - id: visible ? 'c4r.widgets.legend.showLayer' : 'c4r.widgets.legend.hideLayer' - })} - > - <Switch checked={visible} onChange={onChangeVisibility} /> - </Tooltip> - )} - </GridHeader> - ); -} - -export default LegendWrapper; diff --git a/packages/react-ui/src/widgets/legend/Note.js b/packages/react-ui/src/widgets/legend/Note.js deleted file mode 100644 index b2e44d74e..000000000 --- a/packages/react-ui/src/widgets/legend/Note.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Box } from '@mui/material'; -import React from 'react'; -import Typography from '../../components/atoms/Typography'; - -export default function Note({ children }) { - if (!children) { - return null; - } - - return ( - <Box mt={1} data-testid='note-legend'> - <Typography variant='caption'>Note:</Typography>{' '} - <Typography variant='caption'>{children}</Typography> - </Box> - ); -} diff --git a/packages/react-ui/src/widgets/new-legend/legend-types/LegendCategories.js b/packages/react-ui/src/widgets/legend/legend-types/LegendCategories.js similarity index 100% rename from packages/react-ui/src/widgets/new-legend/legend-types/LegendCategories.js rename to packages/react-ui/src/widgets/legend/legend-types/LegendCategories.js diff --git a/packages/react-ui/src/widgets/new-legend/legend-types/LegendIcon.js b/packages/react-ui/src/widgets/legend/legend-types/LegendIcon.js similarity index 100% rename from packages/react-ui/src/widgets/new-legend/legend-types/LegendIcon.js rename to packages/react-ui/src/widgets/legend/legend-types/LegendIcon.js diff --git a/packages/react-ui/src/widgets/new-legend/legend-types/LegendProportion.js b/packages/react-ui/src/widgets/legend/legend-types/LegendProportion.js similarity index 100% rename from packages/react-ui/src/widgets/new-legend/legend-types/LegendProportion.js rename to packages/react-ui/src/widgets/legend/legend-types/LegendProportion.js diff --git a/packages/react-ui/src/widgets/new-legend/legend-types/LegendRamp.js b/packages/react-ui/src/widgets/legend/legend-types/LegendRamp.js similarity index 100% rename from packages/react-ui/src/widgets/new-legend/legend-types/LegendRamp.js rename to packages/react-ui/src/widgets/legend/legend-types/LegendRamp.js diff --git a/packages/react-ui/src/widgets/new-legend/legend-types/LegendTypes.js b/packages/react-ui/src/widgets/legend/legend-types/LegendTypes.js similarity index 100% rename from packages/react-ui/src/widgets/new-legend/legend-types/LegendTypes.js rename to packages/react-ui/src/widgets/legend/legend-types/LegendTypes.js diff --git a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js deleted file mode 100644 index 6f53fb1fe..000000000 --- a/packages/react-ui/src/widgets/new-legend/LegendWidgetUI.js +++ /dev/null @@ -1,198 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { - Box, - Collapse, - Drawer, - IconButton, - Paper, - Tooltip, - Typography -} from '@mui/material'; -import CloseIcon from '@mui/icons-material/Close'; -import LayerIcon from '@mui/icons-material/LayersOutlined'; -import { LEGEND_WIDTH, styles } from './LegendWidgetUI.styles'; -import LegendLayer from './LegendLayer'; -import { useIntl } from 'react-intl'; -import useImperativeIntl from '../../hooks/useImperativeIntl'; - -const EMPTY_OBJ = {}; -const EMPTY_FN = () => {}; -const EMPTY_ARR = []; - -/** - * @param {object} props - * @param {Object.<string, import('./LegendWidgetUI').CustomLegendComponent>} [props.customLegendTypes] - Allow to customise by default legend types that can be rendered. - * @param {import('./LegendWidgetUI').LegendLayerData[]} [props.layers] - Array of layer objects from redux store. - * @param {boolean} [props.collapsed] - Collapsed state for whole legend widget. - * @param {(collapsed: boolean) => void} [props.onChangeCollapsed] - Callback function for collapsed state change. - * @param {({ id, collapsed }: { id: string, collapsed: boolean }) => void} [props.onChangeLegendRowCollapsed] - Callback function for layer visibility change. - * @param {({ id, opacity }: { id: string, opacity: number }) => void} [props.onChangeOpacity] - Callback function for layer opacity change. - * @param {({ id, visible }: { id: string, visible: boolean }) => void} [props.onChangeVisibility] - Callback function for layer collapsed state change. - * @param {({ id, index, selection }: { id: string, index: number, selection: unknown }) => void} props.onChangeSelection - Callback function for layer variable selection change. - * @param {string} [props.title] - Title of the toggle button when widget is open. - * @param {'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'} [props.position] - Position of the widget. - * @param {number} [props.maxZoom] - Global maximum zoom level for the map. - * @param {number} [props.minZoom] - Global minimum zoom level for the map. - * @param {number} [props.currentZoom] - Current zoom level for the map. - * @param {boolean} [props.isMobile] - Whether the widget is displayed on a mobile device. - * @returns {React.ReactNode} - */ -function LegendWidgetUI({ - customLegendTypes = EMPTY_OBJ, - layers = EMPTY_ARR, - collapsed = false, - onChangeCollapsed = EMPTY_FN, - onChangeVisibility = EMPTY_FN, - onChangeOpacity = EMPTY_FN, - onChangeLegendRowCollapsed = EMPTY_FN, - onChangeSelection = EMPTY_FN, - title, - position = 'bottom-right', - maxZoom = 21, - minZoom = 0, - currentZoom, - isMobile -} = {}) { - const intl = useIntl(); - const intlConfig = useImperativeIntl(intl); - const isSingle = layers.length === 1; - - const rootSx = { - ...styles[position], - ...styles.root, - width: collapsed || isMobile ? undefined : LEGEND_WIDTH - }; - - const legendToggleHeader = ( - <Box - sx={{ - ...styles.legendToggle, - ...(!collapsed && styles.legendToggleOpen) - }} - > - <Typography variant='caption' sx={{ flexGrow: 1 }}> - {title} - </Typography> - <Tooltip title={intlConfig.formatMessage({ id: 'c4r.widgets.legend.close' })}> - <IconButton size='small' onClick={() => onChangeCollapsed(true)}> - <CloseIcon /> - </IconButton> - </Tooltip> - </Box> - ); - const legendToggleButton = ( - <Tooltip title={intlConfig.formatMessage({ id: 'c4r.widgets.legend.open' })}> - <IconButton aria-label={title} onClick={() => onChangeCollapsed(false)}> - <LayerIcon /> - </IconButton> - </Tooltip> - ); - - if (isSingle) { - return ( - <Paper elevation={3} sx={rootSx}> - <Box style={styles.legendItemList}> - <LegendLayer - layer={layers[0]} - onChangeCollapsed={onChangeLegendRowCollapsed} - onChangeOpacity={onChangeOpacity} - onChangeVisibility={onChangeVisibility} - onChangeSelection={onChangeSelection} - maxZoom={maxZoom} - minZoom={minZoom} - currentZoom={currentZoom} - customLegendTypes={customLegendTypes} - /> - </Box> - </Paper> - ); - } - - return ( - <Paper elevation={3} sx={rootSx}> - {isMobile ? ( - <> - {legendToggleButton} - <Drawer - anchor='bottom' - open={!collapsed} - onClose={() => onChangeCollapsed(true)} - > - {legendToggleHeader} - <Box style={styles.legendItemList}> - {layers.map((l) => ( - <LegendLayer - key={l.id} - layer={l} - onChangeCollapsed={onChangeLegendRowCollapsed} - onChangeOpacity={onChangeOpacity} - onChangeVisibility={onChangeVisibility} - onChangeSelection={onChangeSelection} - maxZoom={maxZoom} - minZoom={minZoom} - currentZoom={currentZoom} - customLegendTypes={customLegendTypes} - /> - ))} - </Box> - </Drawer> - </> - ) : ( - <> - {collapsed ? legendToggleButton : legendToggleHeader} - <Box sx={{ ...styles.legendItemList, width: collapsed ? 0 : undefined }}> - <Collapse unmountOnExit in={!collapsed} timeout={500}> - {layers.map((l) => ( - <LegendLayer - key={l.id} - layer={l} - onChangeCollapsed={onChangeLegendRowCollapsed} - onChangeOpacity={onChangeOpacity} - onChangeVisibility={onChangeVisibility} - onChangeSelection={onChangeSelection} - maxZoom={maxZoom} - minZoom={minZoom} - currentZoom={currentZoom} - customLegendTypes={customLegendTypes} - /> - ))} - </Collapse> - </Box> - </> - )} - </Paper> - ); -} - -LegendWidgetUI.defaultProps = { - layers: EMPTY_ARR, - customLegendTypes: EMPTY_OBJ, - collapsed: false, - title: 'Layers', - position: 'bottom-right', - onChangeCollapsed: EMPTY_FN, - onChangeLegendRowCollapsed: EMPTY_FN, - onChangeVisibility: EMPTY_FN, - onChangeOpacity: EMPTY_FN, - onChangeSelection: EMPTY_FN -}; - -LegendWidgetUI.propTypes = { - customLegendTypes: PropTypes.objectOf(PropTypes.func), - layers: PropTypes.array, - collapsed: PropTypes.bool, - onChangeCollapsed: PropTypes.func, - onChangeLegendRowCollapsed: PropTypes.func, - onChangeVisibility: PropTypes.func, - onChangeOpacity: PropTypes.func, - onChangeSelection: PropTypes.func, - title: PropTypes.string, - position: PropTypes.oneOf(['top-left', 'top-right', 'bottom-left', 'bottom-right']), - maxZoom: PropTypes.number, - minZoom: PropTypes.number, - currentZoom: PropTypes.number, - isMobile: PropTypes.bool -}; - -export default LegendWidgetUI; diff --git a/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js b/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js index 8501eaf14..878fd9a5e 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js @@ -1,5 +1,5 @@ import { useReducer, useState } from 'react'; -import LegendWidgetUI from '../../../src/widgets/new-legend/LegendWidgetUI'; +import LegendWidgetUI from '../../../src/widgets/legend/LegendWidgetUI'; import { IntlProvider } from 'react-intl'; import { Box } from '@mui/material'; import { fixtures } from './legendFixtures'; diff --git a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendCategories.stories.js b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendCategories.stories.js index 052b4d994..a14d30e13 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendCategories.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendCategories.stories.js @@ -1,5 +1,5 @@ import React from 'react'; -import LegendCategories from '../../../../src/widgets/new-legend/legend-types/LegendCategories'; +import LegendCategories from '../../../../src/widgets/legend/legend-types/LegendCategories'; const DEFAULT_LEGEND = { legend: { diff --git a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendIcon.stories.js b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendIcon.stories.js index 87d865364..6dd9b02ed 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendIcon.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendIcon.stories.js @@ -1,5 +1,5 @@ import React from 'react'; -import LegendIcon from '../../../../src/widgets/new-legend/legend-types/LegendIcon'; +import LegendIcon from '../../../../src/widgets/legend/legend-types/LegendIcon'; const ICON = `data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICAgIDxkZWZzPgogICAgICAgIDxmaWx0ZXIgaWQ9InFsbTY3aXI1MWEiPgogICAgICAgICAgICA8ZmVDb2xvck1hdHJpeCBpbj0iU291cmNlR3JhcGhpYyIgdmFsdWVzPSIwIDAgMCAwIDAuMTcyNTQ5IDAgMCAwIDAgMC4xODgyMzUgMCAwIDAgMCAwLjE5NjA3OCAwIDAgMCAxLjAwMDAwMCAwIi8+CiAgICAgICAgPC9maWx0ZXI+CiAgICA8L2RlZnM+CiAgICA8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnPgogICAgICAgICAgICA8ZyBmaWx0ZXI9InVybCgjcWxtNjdpcjUxYSkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yNDIgLTQ2NCkgdHJhbnNsYXRlKDIyMiA0NDgpIj4KICAgICAgICAgICAgICAgIDxnIGZpbGw9IiMyQzMwMzIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIwIDE2KSI+CiAgICAgICAgICAgICAgICAgICAgPHBhdGggZD0iTTEyIDBjNi42MjcgMCAxMiA1LjM3MyAxMiAxMnMtNS4zNzMgMTItMTIgMTJTMCAxOC42MjcgMCAxMiA1LjM3MyAwIDEyIDB6IiBvcGFjaXR5PSIuMiIvPgogICAgICAgICAgICAgICAgICAgIDxjaXJjbGUgY3g9IjEyIiBjeT0iMTIiIHI9IjQiLz4KICAgICAgICAgICAgICAgIDwvZz4KICAgICAgICAgICAgPC9nPgogICAgICAgIDwvZz4KICAgIDwvZz4KPC9zdmc+Cg==`; diff --git a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendProportion.stories.js b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendProportion.stories.js index c3adc085f..2ba9cd02f 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendProportion.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendProportion.stories.js @@ -1,5 +1,5 @@ import React from 'react'; -import LegendProportion from '../../../../src/widgets/new-legend/legend-types/LegendProportion'; +import LegendProportion from '../../../../src/widgets/legend/legend-types/LegendProportion'; import { IntlProvider } from 'react-intl'; const DEFAULT_LEGEND = { diff --git a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendRamp.stories.js b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendRamp.stories.js index b95537f4b..7d7de78e9 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/legend/LegendRamp.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/legend/LegendRamp.stories.js @@ -1,5 +1,5 @@ import React from 'react'; -import LegendRamp from '../../../../src/widgets/new-legend/legend-types/LegendRamp'; +import LegendRamp from '../../../../src/widgets/legend/legend-types/LegendRamp'; const DEFAULT_LEGEND = { legend: { diff --git a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js index 6318efd7f..8d805969b 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js +++ b/packages/react-ui/storybook/stories/widgetsUI/legendFixtures.js @@ -1,4 +1,4 @@ -import LEGEND_TYPES from '../../../src/widgets/new-legend/legend-types/LegendTypes'; +import LEGEND_TYPES from '../../../src/widgets/legend/legend-types/LegendTypes'; export const fixtures = [ { From a0b047e7dc5865318c31e34c8852280149b23d5f Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Fri, 9 Feb 2024 18:15:07 +0100 Subject: [PATCH 43/66] WIP move sx to styled components --- .../src/widgets/legend/LegendWidgetUI.js | 43 +++------- .../widgets/legend/LegendWidgetUI.styles.js | 81 ++++++------------- 2 files changed, 36 insertions(+), 88 deletions(-) diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/legend/LegendWidgetUI.js index 6f53fb1fe..28db35ca4 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.js @@ -1,17 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { - Box, - Collapse, - Drawer, - IconButton, - Paper, - Tooltip, - Typography -} from '@mui/material'; +import { Box, Collapse, Drawer, IconButton, Tooltip, Typography } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; import LayerIcon from '@mui/icons-material/LayersOutlined'; -import { LEGEND_WIDTH, styles } from './LegendWidgetUI.styles'; +import { LegendRoot, LegendToggleHeader, styles } from './LegendWidgetUI.styles'; import LegendLayer from './LegendLayer'; import { useIntl } from 'react-intl'; import useImperativeIntl from '../../hooks/useImperativeIntl'; @@ -31,11 +23,11 @@ const EMPTY_ARR = []; * @param {({ id, visible }: { id: string, visible: boolean }) => void} [props.onChangeVisibility] - Callback function for layer collapsed state change. * @param {({ id, index, selection }: { id: string, index: number, selection: unknown }) => void} props.onChangeSelection - Callback function for layer variable selection change. * @param {string} [props.title] - Title of the toggle button when widget is open. - * @param {'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'} [props.position] - Position of the widget. * @param {number} [props.maxZoom] - Global maximum zoom level for the map. * @param {number} [props.minZoom] - Global minimum zoom level for the map. * @param {number} [props.currentZoom] - Current zoom level for the map. * @param {boolean} [props.isMobile] - Whether the widget is displayed on a mobile device. + * @param {import('@mui/system').SxProps<import('@mui/system').Theme>} [props.sx] - Style object for the root component. * @returns {React.ReactNode} */ function LegendWidgetUI({ @@ -48,29 +40,18 @@ function LegendWidgetUI({ onChangeLegendRowCollapsed = EMPTY_FN, onChangeSelection = EMPTY_FN, title, - position = 'bottom-right', maxZoom = 21, minZoom = 0, currentZoom, - isMobile + isMobile, + sx } = {}) { const intl = useIntl(); const intlConfig = useImperativeIntl(intl); const isSingle = layers.length === 1; - const rootSx = { - ...styles[position], - ...styles.root, - width: collapsed || isMobile ? undefined : LEGEND_WIDTH - }; - const legendToggleHeader = ( - <Box - sx={{ - ...styles.legendToggle, - ...(!collapsed && styles.legendToggleOpen) - }} - > + <LegendToggleHeader collapsed={collapsed}> <Typography variant='caption' sx={{ flexGrow: 1 }}> {title} </Typography> @@ -79,7 +60,7 @@ function LegendWidgetUI({ <CloseIcon /> </IconButton> </Tooltip> - </Box> + </LegendToggleHeader> ); const legendToggleButton = ( <Tooltip title={intlConfig.formatMessage({ id: 'c4r.widgets.legend.open' })}> @@ -91,7 +72,7 @@ function LegendWidgetUI({ if (isSingle) { return ( - <Paper elevation={3} sx={rootSx}> + <LegendRoot sx={sx} elevation={3} collapsed={collapsed || isMobile}> <Box style={styles.legendItemList}> <LegendLayer layer={layers[0]} @@ -105,12 +86,12 @@ function LegendWidgetUI({ customLegendTypes={customLegendTypes} /> </Box> - </Paper> + </LegendRoot> ); } return ( - <Paper elevation={3} sx={rootSx}> + <LegendRoot sx={sx} elevation={3} collapsed={collapsed || isMobile}> {isMobile ? ( <> {legendToggleButton} @@ -161,7 +142,7 @@ function LegendWidgetUI({ </Box> </> )} - </Paper> + </LegendRoot> ); } @@ -170,7 +151,6 @@ LegendWidgetUI.defaultProps = { customLegendTypes: EMPTY_OBJ, collapsed: false, title: 'Layers', - position: 'bottom-right', onChangeCollapsed: EMPTY_FN, onChangeLegendRowCollapsed: EMPTY_FN, onChangeVisibility: EMPTY_FN, @@ -188,7 +168,6 @@ LegendWidgetUI.propTypes = { onChangeOpacity: PropTypes.func, onChangeSelection: PropTypes.func, title: PropTypes.string, - position: PropTypes.oneOf(['top-left', 'top-right', 'bottom-left', 'bottom-right']), maxZoom: PropTypes.number, minZoom: PropTypes.number, currentZoom: PropTypes.number, diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js b/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js index 077879bbf..3adc068dd 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js @@ -1,25 +1,32 @@ +import { Box, Paper, styled } from '@mui/material'; import { ICON_SIZE_MEDIUM } from '../../theme/themeConstants'; export const LEGEND_WIDTH = 240; + +export const LegendRoot = styled(Paper, { + shouldForwardProp: (prop) => !['collapsed'].includes(prop) +})(({ theme, collapsed }) => ({ + width: collapsed ? undefined : LEGEND_WIDTH, + background: theme.palette.background.paper, + position: 'absolute', + maxHeight: 'calc(100% - 120px)', + display: 'flex', + flexDirection: 'column' +})); + +export const LegendToggleHeader = styled(Box, { + shouldForwardProp: (prop) => !['collapsed'].includes(prop) +})(({ theme, collapsed }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + pl: 2, + pr: 1, + py: 1, + borderBottom: collapsed ? undefined : `1px solid ${theme.palette.divider}` +})); + export const styles = { - root: { - background: (theme) => theme.palette.background.paper, - position: 'absolute', - maxHeight: 'calc(100% - 120px)', - display: 'flex', - flexDirection: 'column' - }, - legendToggleOpen: { - borderBottom: (theme) => `1px solid ${theme.palette.divider}` - }, - legendToggle: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - pl: 2, - pr: 1, - py: 1 - }, legendItemList: { overflow: 'auto', maxHeight: `calc(100% - 12px)` @@ -40,28 +47,6 @@ export const styles = { top: 0, background: (theme) => theme.palette.background.paper }, - legendItemBody: { - px: 2 - // '& [data-testid="categories-legend"] > .MuiGrid-root': { - // paddingTop: '6px', - // paddingBottom: '6px' - // }, - // '& [data-testid="icon-legend"] > .MuiGrid-root': { - // paddingTop: '2px', - // paddingBottom: '2px', - // '& > .MuiBox-root': { - // width: '20px', - // height: '20px', - // marginRight: '8px' - // }, - // '& img': { - // display: 'block', - // margin: 'auto', - // width: 'auto', - // height: '20px' - // } - // } - }, layerVariablesList: { display: 'flex', flexDirection: 'column', @@ -104,21 +89,5 @@ export const styles = { margin: 'auto', display: 'block' } - }, - 'top-left': { - top: 0, - left: 0 - }, - 'top-right': { - top: 0, - right: 0 - }, - 'bottom-left': { - bottom: 0, - left: 0 - }, - 'bottom-right': { - bottom: 0, - right: 0 } }; From 51f127e83365b4cb45b005e6894ee606b772ad34 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Fri, 9 Feb 2024 18:22:04 +0100 Subject: [PATCH 44/66] more styled components --- .../src/widgets/legend/LegendLayer.js | 6 +-- .../widgets/legend/LegendOpacityControl.js | 7 ++-- .../widgets/legend/LegendWidgetUI.styles.js | 42 ++++++++++--------- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/packages/react-ui/src/widgets/legend/LegendLayer.js b/packages/react-ui/src/widgets/legend/LegendLayer.js index ee636a8a5..f801657fe 100644 --- a/packages/react-ui/src/widgets/legend/LegendLayer.js +++ b/packages/react-ui/src/widgets/legend/LegendLayer.js @@ -5,7 +5,7 @@ import EyeIcon from '@mui/icons-material/VisibilityOutlined'; import EyeOffIcon from '@mui/icons-material/VisibilityOffOutlined'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; -import { styles } from './LegendWidgetUI.styles'; +import { LegendItemHeader, styles } from './LegendWidgetUI.styles'; import LegendOpacityControl from './LegendOpacityControl'; import LegendLayerTitle from './LegendLayerTitle'; import LegendLayerVariable from './LegendLayerVariable'; @@ -88,7 +88,7 @@ export default function LegendLayer({ } }} > - <Box ref={menuAnchorRef} component='header' sx={styles.legendItemHeader}> + <LegendItemHeader ref={menuAnchorRef}> {collapsible && ( <IconButton size='small' @@ -151,7 +151,7 @@ export default function LegendLayer({ </IconButton> </Tooltip> )} - </Box> + </LegendItemHeader> <Collapse unmountOnExit timeout={100} in={isExpanded}> <Box data-testid='legend-layer-variable-list' diff --git a/packages/react-ui/src/widgets/legend/LegendOpacityControl.js b/packages/react-ui/src/widgets/legend/LegendOpacityControl.js index 4c17074b0..45dec84e9 100644 --- a/packages/react-ui/src/widgets/legend/LegendOpacityControl.js +++ b/packages/react-ui/src/widgets/legend/LegendOpacityControl.js @@ -1,6 +1,5 @@ import React from 'react'; import { - Box, IconButton, InputAdornment, Popover, @@ -8,7 +7,7 @@ import { TextField, Tooltip } from '@mui/material'; -import { styles } from './LegendWidgetUI.styles'; +import { StyledOpacityControl, styles } from './LegendWidgetUI.styles'; import { useIntl } from 'react-intl'; import useImperativeIntl from '../../hooks/useImperativeIntl'; import OpacityIcon from '../../assets/icons/OpacityIcon'; @@ -67,7 +66,7 @@ export default function LegendOpacityControl({ } }} > - <Box sx={styles.opacityControl}> + <StyledOpacityControl> <Slider value={opacity * 100} onChange={(_, v) => onChange(v / 100)} @@ -97,7 +96,7 @@ export default function LegendOpacityControl({ ) }} /> - </Box> + </StyledOpacityControl> </Popover> </> ); diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js b/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js index 3adc068dd..36befb343 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js @@ -1,4 +1,4 @@ -import { Box, Paper, styled } from '@mui/material'; +import { Paper, styled } from '@mui/material'; import { ICON_SIZE_MEDIUM } from '../../theme/themeConstants'; export const LEGEND_WIDTH = 240; @@ -14,7 +14,7 @@ export const LegendRoot = styled(Paper, { flexDirection: 'column' })); -export const LegendToggleHeader = styled(Box, { +export const LegendToggleHeader = styled('header', { shouldForwardProp: (prop) => !['collapsed'].includes(prop) })(({ theme, collapsed }) => ({ display: 'flex', @@ -26,6 +26,26 @@ export const LegendToggleHeader = styled(Box, { borderBottom: collapsed ? undefined : `1px solid ${theme.palette.divider}` })); +export const LegendItemHeader = styled('header')(({ theme }) => ({ + p: 1.5, + pr: 2, + gap: 0.5, + display: 'flex', + justifyContent: 'space-between', + position: 'sticky', + zIndex: 2, + top: 0, + background: theme.palette.background.paper +})); + +export const StyledOpacityControl = styled('div')(() => ({ + display: 'flex', + gap: 2, + alignItems: 'center', + p: 1, + minWidth: LEGEND_WIDTH - 32 +})); + export const styles = { legendItemList: { overflow: 'auto', @@ -36,29 +56,11 @@ export const styles = { borderTop: (theme) => `1px solid ${theme.palette.divider}` } }, - legendItemHeader: { - p: 1.5, - pr: 2, - gap: 0.5, - display: 'flex', - justifyContent: 'space-between', - position: 'sticky', - zIndex: 2, - top: 0, - background: (theme) => theme.palette.background.paper - }, layerVariablesList: { display: 'flex', flexDirection: 'column', gap: 1 }, - opacityControl: { - display: 'flex', - gap: 2, - alignItems: 'center', - p: 1, - minWidth: LEGEND_WIDTH - 32 - }, layerOptions: { background: (theme) => theme.palette.background.default, px: 2, From 8d44ee8ee88ccbfff68ac98b4621c74140f06e69 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Fri, 9 Feb 2024 18:25:15 +0100 Subject: [PATCH 45/66] fix styled components --- .../src/widgets/legend/LegendWidgetUI.styles.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js b/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js index 36befb343..9fa0f4d5d 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js @@ -20,16 +20,15 @@ export const LegendToggleHeader = styled('header', { display: 'flex', alignItems: 'center', justifyContent: 'space-between', - pl: 2, - pr: 1, - py: 1, + padding: theme.spacing(1), + paddingLeft: theme.spacing(2), borderBottom: collapsed ? undefined : `1px solid ${theme.palette.divider}` })); export const LegendItemHeader = styled('header')(({ theme }) => ({ - p: 1.5, - pr: 2, - gap: 0.5, + padding: theme.spacing(1.5), + paddingRight: theme.spacing(2), + gap: theme.spacing(0.5), display: 'flex', justifyContent: 'space-between', position: 'sticky', @@ -38,11 +37,11 @@ export const LegendItemHeader = styled('header')(({ theme }) => ({ background: theme.palette.background.paper })); -export const StyledOpacityControl = styled('div')(() => ({ +export const StyledOpacityControl = styled('div')(({ theme }) => ({ display: 'flex', - gap: 2, + gap: theme.spacing(2), alignItems: 'center', - p: 1, + padding: theme.spacing(1), minWidth: LEGEND_WIDTH - 32 })); From 49c4e4ded37aba7162ad7ba156111bf7a9816d51 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Fri, 9 Feb 2024 18:55:19 +0100 Subject: [PATCH 46/66] more styled components refactor --- .../__tests__/widgets/LegendWidgetUI.test.js | 23 ----- packages/react-ui/src/localization/es.js | 21 ++++- packages/react-ui/src/localization/id.js | 35 +++++-- .../src/widgets/legend/LegendLayer.js | 11 +-- .../widgets/legend/LegendOpacityControl.js | 14 +-- .../src/widgets/legend/LegendWidgetUI.js | 16 ++-- .../widgets/legend/LegendWidgetUI.styles.js | 91 +++++++++---------- .../legend/legend-types/LegendCategories.js | 47 ++++------ .../widgets/legend/legend-types/LegendIcon.js | 12 +-- 9 files changed, 130 insertions(+), 140 deletions(-) diff --git a/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js b/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js index 9b4214fed..a58b07d95 100644 --- a/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js +++ b/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js @@ -234,29 +234,6 @@ describe('LegendWidgetUI', () => { test('helper text', () => { render(<Widget layers={[{ ...DATA[0], helperText: 'helperText' }]}></Widget>); - expect(screen.getByText('helperText')).toBeInTheDocument(); }); - - // test('with custom layer options', async () => { - // const layer = DATA[8]; - // render( - // <Widget layers={[layer]} customLayerOptions={LAYER_OPTIONS_COMPONENTS}></Widget> - // ); - // const layerOptionsBtn = await screen.findByLabelText('Layer options'); - // expect(layerOptionsBtn).toBeInTheDocument(); - // layerOptionsBtn.click(); - // expect(screen.getByText('PaletteSelector')).toBeInTheDocument(); - // }); - - // test('with custom layer options - unknown option', async () => { - // const layer = { ...DATA[8], options: ['unknown'] }; - // render( - // <Widget layers={[layer]} customLayerOptions={LAYER_OPTIONS_COMPONENTS}></Widget> - // ); - // const layerOptionsBtn = await screen.findByLabelText('Layer options'); - // expect(layerOptionsBtn).toBeInTheDocument(); - // layerOptionsBtn.click(); - // expect(screen.getByText('Unknown layer option')).toBeInTheDocument(); - // }); }); diff --git a/packages/react-ui/src/localization/es.js b/packages/react-ui/src/localization/es.js index de111fd05..43e4cc505 100644 --- a/packages/react-ui/src/localization/es.js +++ b/packages/react-ui/src/localization/es.js @@ -39,7 +39,26 @@ const locales = { layer: 'capa', opacity: 'Opacidad', hideLayer: 'Ocultar capa', - showLayer: 'Mostrar capa' + showLayer: 'Mostrar capa', + open: 'Abrir leyenda', + close: 'Cerrar', + collapse: 'Colapsar capa', + expand: 'Expandir capa', + zoomLevel: 'Nivel de zoom', + zoomLevelTooltip: 'Esta capa solo es visible a ciertos niveles de zoom', + lowerThan: 'menor que', + greaterThan: 'mayor que', + and: 'y', + zoomNote: 'Nota: esta capa se mostrará a un nivel de zoom', + notSupported: 'no es un tipo de leyenda conocido', + subtitles: { + proportion: 'Tamaño basado en', + icon: 'Icono basado en', + strokeColor: 'Color del borde basado en', + color: 'Color basado en' + }, + max: 'Max', + min: 'Min' }, range: { clear: 'Limpiar', diff --git a/packages/react-ui/src/localization/id.js b/packages/react-ui/src/localization/id.js index 395e5bb11..b01d1e6a2 100644 --- a/packages/react-ui/src/localization/id.js +++ b/packages/react-ui/src/localization/id.js @@ -33,14 +33,33 @@ const locales = { clear: 'Bersihkan' }, legend: { - by: 'Dengan {attr}', - layerOptions: 'Opsi lapisan', - hide: 'Sembunyikan', - show: 'Tampilkan', - layer: 'lapisan', - opacity: 'Opasitas', - hideLayer: 'Sembunyikan lapisan', - showLayer: 'Tampilkan lapisan' + by: 'By {attr}', + layerOptions: 'Layer options', + hide: 'Hide', + show: 'Show', + layer: 'layer', + opacity: 'Opacity', + hideLayer: 'Hide layer', + showLayer: 'Show layer', + open: 'Open legend', + close: 'Close', + collapse: 'Collapse layer', + expand: 'Expand layer', + zoomLevel: 'Zoom level', + zoomLevelTooltip: 'This layer is only visible at certain zoom levels', + lowerThan: 'lower than', + greaterThan: 'greater than', + and: 'and', + zoomNote: 'Note: this layer will display at zoom levels', + notSupported: 'is not a known legend type', + subtitles: { + proportion: 'Radius range by', + icon: 'Icon based on', + strokeColor: 'Stroke color based on', + color: 'Color based on' + }, + max: 'Max', + min: 'Min' }, range: { clear: 'Bersihkan', diff --git a/packages/react-ui/src/widgets/legend/LegendLayer.js b/packages/react-ui/src/widgets/legend/LegendLayer.js index f801657fe..091351281 100644 --- a/packages/react-ui/src/widgets/legend/LegendLayer.js +++ b/packages/react-ui/src/widgets/legend/LegendLayer.js @@ -5,7 +5,7 @@ import EyeIcon from '@mui/icons-material/VisibilityOutlined'; import EyeOffIcon from '@mui/icons-material/VisibilityOffOutlined'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; -import { LegendItemHeader, styles } from './LegendWidgetUI.styles'; +import { LayerVariablesList, LegendItemHeader } from './LegendWidgetUI.styles'; import LegendOpacityControl from './LegendOpacityControl'; import LegendLayerTitle from './LegendLayerTitle'; import LegendLayerVariable from './LegendLayerVariable'; @@ -153,12 +153,9 @@ export default function LegendLayer({ )} </LegendItemHeader> <Collapse unmountOnExit timeout={100} in={isExpanded}> - <Box + <LayerVariablesList data-testid='legend-layer-variable-list' - sx={{ - ...styles.layerVariablesList, - opacity: outsideCurrentZoom ? 0.5 : 1 - }} + opacity={outsideCurrentZoom ? 0.5 : 1} > {legendLayerVariables.map((legend, index) => ( <LegendLayerVariable @@ -171,7 +168,7 @@ export default function LegendLayer({ } /> ))} - </Box> + </LayerVariablesList> {helperText && ( <Typography variant='caption' color='textSecondary' component='p' sx={{ p: 2 }}> {helperText} diff --git a/packages/react-ui/src/widgets/legend/LegendOpacityControl.js b/packages/react-ui/src/widgets/legend/LegendOpacityControl.js index 45dec84e9..c2b2a3ff7 100644 --- a/packages/react-ui/src/widgets/legend/LegendOpacityControl.js +++ b/packages/react-ui/src/widgets/legend/LegendOpacityControl.js @@ -1,13 +1,6 @@ import React from 'react'; -import { - IconButton, - InputAdornment, - Popover, - Slider, - TextField, - Tooltip -} from '@mui/material'; -import { StyledOpacityControl, styles } from './LegendWidgetUI.styles'; +import { IconButton, InputAdornment, Popover, Slider, Tooltip } from '@mui/material'; +import { OpacityTextField, StyledOpacityControl } from './LegendWidgetUI.styles'; import { useIntl } from 'react-intl'; import useImperativeIntl from '../../hooks/useImperativeIntl'; import OpacityIcon from '../../assets/icons/OpacityIcon'; @@ -74,12 +67,11 @@ export default function LegendOpacityControl({ max={100} step={1} /> - <TextField + <OpacityTextField size='small' type='number' value={Math.round(opacity * 100)} onChange={handleTextFieldChange} - sx={styles.opacityInput} inputProps={{ step: 1, min: 0, diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/legend/LegendWidgetUI.js index 28db35ca4..ec106e6ff 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.js @@ -1,9 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Box, Collapse, Drawer, IconButton, Tooltip, Typography } from '@mui/material'; +import { Collapse, Drawer, IconButton, Tooltip, Typography } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; import LayerIcon from '@mui/icons-material/LayersOutlined'; -import { LegendRoot, LegendToggleHeader, styles } from './LegendWidgetUI.styles'; +import { LegendContent, LegendRoot, LegendToggleHeader } from './LegendWidgetUI.styles'; import LegendLayer from './LegendLayer'; import { useIntl } from 'react-intl'; import useImperativeIntl from '../../hooks/useImperativeIntl'; @@ -73,7 +73,7 @@ function LegendWidgetUI({ if (isSingle) { return ( <LegendRoot sx={sx} elevation={3} collapsed={collapsed || isMobile}> - <Box style={styles.legendItemList}> + <LegendContent> <LegendLayer layer={layers[0]} onChangeCollapsed={onChangeLegendRowCollapsed} @@ -85,7 +85,7 @@ function LegendWidgetUI({ currentZoom={currentZoom} customLegendTypes={customLegendTypes} /> - </Box> + </LegendContent> </LegendRoot> ); } @@ -101,7 +101,7 @@ function LegendWidgetUI({ onClose={() => onChangeCollapsed(true)} > {legendToggleHeader} - <Box style={styles.legendItemList}> + <LegendContent> {layers.map((l) => ( <LegendLayer key={l.id} @@ -116,13 +116,13 @@ function LegendWidgetUI({ customLegendTypes={customLegendTypes} /> ))} - </Box> + </LegendContent> </Drawer> </> ) : ( <> {collapsed ? legendToggleButton : legendToggleHeader} - <Box sx={{ ...styles.legendItemList, width: collapsed ? 0 : undefined }}> + <LegendContent width={collapsed ? 0 : undefined}> <Collapse unmountOnExit in={!collapsed} timeout={500}> {layers.map((l) => ( <LegendLayer @@ -139,7 +139,7 @@ function LegendWidgetUI({ /> ))} </Collapse> - </Box> + </LegendContent> </> )} </LegendRoot> diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js b/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js index 9fa0f4d5d..1330e0f46 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js @@ -1,4 +1,4 @@ -import { Paper, styled } from '@mui/material'; +import { Box, Paper, TextField, styled } from '@mui/material'; import { ICON_SIZE_MEDIUM } from '../../theme/themeConstants'; export const LEGEND_WIDTH = 240; @@ -45,50 +45,47 @@ export const StyledOpacityControl = styled('div')(({ theme }) => ({ minWidth: LEGEND_WIDTH - 32 })); -export const styles = { - legendItemList: { - overflow: 'auto', - maxHeight: `calc(100% - 12px)` - }, - legendItem: { - '&:not(:first-of-type)': { - borderTop: (theme) => `1px solid ${theme.palette.divider}` - } - }, - layerVariablesList: { - display: 'flex', - flexDirection: 'column', - gap: 1 - }, - layerOptions: { - background: (theme) => theme.palette.background.default, - px: 2, - py: 1, - m: 2 - }, - opacityInput: { - display: 'flex', - width: '60px', - flexShrink: 0 - }, - legendVariableList: { - m: 0, - p: 0, - pb: 1, - display: 'flex', - flexDirection: 'column' - }, - legendVariableListItem: { - display: 'flex', - alignItems: 'center' - }, - legendIconWrapper: { - mr: 1.5, - width: ICON_SIZE_MEDIUM, - height: ICON_SIZE_MEDIUM, - '& img': { - margin: 'auto', - display: 'block' - } +export const OpacityTextField = styled(TextField)(({ theme }) => ({ + display: 'flex', + width: theme.spacing(7.5), + flexShrink: 0 +})); + +export const LayerVariablesList = styled('ul', { + shouldForwardProp: (prop) => !['opacity'].includes(prop) +})(({ theme, opacity }) => ({ + opacity, + margin: 0, + padding: 0, + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1) +})); + +export const LegendVariableList = styled('ul')(({ theme }) => ({ + margin: 0, + padding: 0, + paddingBottom: theme.spacing(1), + display: 'flex', + flexDirection: 'column' +})); + +export const LegendIconWrapper = styled('div')(({ theme }) => ({ + marginRight: theme.spacing(1.5), + width: ICON_SIZE_MEDIUM, + height: ICON_SIZE_MEDIUM, + '& img': { + margin: 'auto', + display: 'block' } -}; +})); + +export const LegendContent = styled(Box, { + shouldForwardProp: (prop) => !['width'].includes(prop) +})(({ width }) => ({ + width, + overflow: 'auto', + maxHeight: `calc(100% - 12px)` +})); + +export const styles = {}; diff --git a/packages/react-ui/src/widgets/legend/legend-types/LegendCategories.js b/packages/react-ui/src/widgets/legend/legend-types/LegendCategories.js index a9ff6faec..d3a929886 100644 --- a/packages/react-ui/src/widgets/legend/legend-types/LegendCategories.js +++ b/packages/react-ui/src/widgets/legend/legend-types/LegendCategories.js @@ -1,9 +1,9 @@ import React from 'react'; -import { Box, Tooltip, styled } from '@mui/material'; +import { Box, styled } from '@mui/material'; import { getPalette } from '../../../utils/palette'; import PropTypes from 'prop-types'; import LegendLayerTitle from '../LegendLayerTitle'; -import { styles } from '../LegendWidgetUI.styles'; +import { LegendVariableList } from '../LegendWidgetUI.styles'; /** * @param {object} props @@ -22,11 +22,10 @@ function LegendCategories({ legend }) { const palette = getPalette(colors, labels.length); return ( - <Box component='ul' data-testid='categories-legend' sx={styles.legendVariableList}> + <LegendVariableList data-testid='categories-legend'> {labels.map((label, idx) => ( <LegendCategoriesRow key={label + idx} - isMax={false} label={label} color={palette[idx]} icon={ @@ -38,7 +37,7 @@ function LegendCategories({ legend }) { isStrokeColor={isStrokeColor} /> ))} - </Box> + </LegendVariableList> ); } @@ -105,8 +104,8 @@ const getIconStyles = ({ icon, color, maskedIcon }) => ({ const Marker = styled(Box, { shouldForwardProp: (prop) => - !['isMax', 'icon', 'maskedIcon', 'color', 'isStrokeColor'].includes(prop) -})(({ isMax, icon, maskedIcon, color, isStrokeColor, theme }) => ({ + !['icon', 'maskedIcon', 'color', 'isStrokeColor'].includes(prop) +})(({ icon, maskedIcon, color, isStrokeColor, theme }) => ({ whiteSpace: 'nowrap', display: 'block', width: theme.spacing(1.5), @@ -116,31 +115,21 @@ const Marker = styled(Box, { border: '2px solid transparent', ...(icon ? getIconStyles({ icon, color, maskedIcon }) - : getCircleStyles({ isMax, color, isStrokeColor, theme })) + : getCircleStyles({ color, isStrokeColor, theme })) })); -function LegendCategoriesRow({ - label, - isMax, - isStrokeColor, - color = '#000', - icon, - maskedIcon -}) { +function LegendCategoriesRow({ label, isStrokeColor, color = '#000', icon, maskedIcon }) { return ( - <Box component='li' sx={styles.legendVariableListItem}> - <Tooltip title={isMax ? 'Most representative' : ''}> - <Marker - className='marker' - mr={1.5} - component='span' - isMax={isMax} - icon={icon} - maskedIcon={maskedIcon} - isStrokeColor={isStrokeColor} - color={color} - /> - </Tooltip> + <Box component='li' sx={{ display: 'flex', alignItems: 'center' }}> + <Marker + className='marker' + mr={1.5} + component='span' + icon={icon} + maskedIcon={maskedIcon} + isStrokeColor={isStrokeColor} + color={color} + /> <LegendLayerTitle title={label} visible diff --git a/packages/react-ui/src/widgets/legend/legend-types/LegendIcon.js b/packages/react-ui/src/widgets/legend/legend-types/LegendIcon.js index 2fc7fb645..e3b72c0ed 100644 --- a/packages/react-ui/src/widgets/legend/legend-types/LegendIcon.js +++ b/packages/react-ui/src/widgets/legend/legend-types/LegendIcon.js @@ -2,7 +2,7 @@ import React from 'react'; import { Box } from '@mui/material'; import PropTypes from 'prop-types'; import { ICON_SIZE_MEDIUM } from '../../../theme/themeConstants'; -import { styles } from '../LegendWidgetUI.styles'; +import { LegendIconWrapper, LegendVariableList } from '../LegendWidgetUI.styles'; import LegendLayerTitle from '../LegendLayerTitle'; /** @@ -13,12 +13,12 @@ import LegendLayerTitle from '../LegendLayerTitle'; function LegendIcon({ legend }) { const { labels = [], icons = [] } = legend; return ( - <Box component='ul' data-testid='icon-legend' sx={styles.legendVariableList}> + <LegendVariableList data-testid='icon-legend'> {labels.map((label, idx) => ( - <Box key={label} component='li' sx={styles.legendVariableListItem}> - <Box sx={styles.legendIconWrapper}> + <Box key={label} component='li' sx={{ display: 'flex', alignItems: 'center' }}> + <LegendIconWrapper> <img src={icons[idx]} alt={label} width='autio' height={ICON_SIZE_MEDIUM} /> - </Box> + </LegendIconWrapper> <LegendLayerTitle visible title={label} @@ -26,7 +26,7 @@ function LegendIcon({ legend }) { /> </Box> ))} - </Box> + </LegendVariableList> ); } From e9ccae42ebd9cc2fdc64fce6d5e62b74d5d7f9cc Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Fri, 9 Feb 2024 19:00:16 +0100 Subject: [PATCH 47/66] remove unused param --- .../storybook/stories/widgetsUI/LegendWidgetUI.stories.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js b/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js index 878fd9a5e..1a2219978 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js @@ -19,13 +19,6 @@ const options = { control: { type: 'array' } - }, - position: { - defaultValue: 'top-left', - options: ['bottom-left', 'bottom-right', 'top-left', 'top-right'], - control: { - type: 'radio' - } } }, parameters: { From 062481bc045663b04a9e646ae71dcae14f982fe8 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Fri, 9 Feb 2024 19:00:31 +0100 Subject: [PATCH 48/66] update default max zoom --- packages/react-ui/src/widgets/legend/LegendWidgetUI.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/legend/LegendWidgetUI.js index ec106e6ff..ba3f23263 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.js @@ -40,7 +40,7 @@ function LegendWidgetUI({ onChangeLegendRowCollapsed = EMPTY_FN, onChangeSelection = EMPTY_FN, title, - maxZoom = 21, + maxZoom = 20, minZoom = 0, currentZoom, isMobile, From 803cbe29de7df6e21c968cd1bea51cc15c4555ec Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Mon, 12 Feb 2024 11:59:31 +0100 Subject: [PATCH 49/66] edge cases for empty and single layers --- .../__tests__/widgets/LegendWidgetUI.test.js | 14 ++++++++++- .../src/widgets/legend/LegendLayer.js | 23 ++++++++++++++++--- .../src/widgets/legend/LegendLayerVariable.js | 2 +- .../src/widgets/legend/LegendWidgetUI.d.ts | 2 +- .../widgetsUI/LegendWidgetUI.stories.js | 8 +++++-- .../react-widgets/src/widgets/LegendWidget.js | 3 ++- 6 files changed, 43 insertions(+), 9 deletions(-) diff --git a/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js b/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js index a58b07d95..76683f29f 100644 --- a/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js +++ b/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js @@ -108,6 +108,17 @@ describe('LegendWidgetUI', () => { expect(screen.queryByTestId('categories-legend')).not.toBeInTheDocument(); }); + test('layer with no legend is not shown on widget', () => { + const layer = { + id: 'test-layer-no-legend', + title: 'Test layer no legend', + visible: true + }; + render(<Widget layers={[layer]}></Widget>); + expect(screen.queryByText('Layers')).toBeInTheDocument(); + expect(screen.queryByText('Test layer no legend')).not.toBeInTheDocument(); + }); + test('Category legend', () => { render(<Widget layers={[DATA[0]]}></Widget>); expect(screen.getByTestId('categories-legend')).toBeInTheDocument(); @@ -174,7 +185,8 @@ describe('LegendWidgetUI', () => { title: 'Test opacity control', visible: true, showOpacityControl: true, - opacity: 0.8 + opacity: 0.8, + legend: {} }; const onChangeOpacity = jest.fn(); const container = render( diff --git a/packages/react-ui/src/widgets/legend/LegendLayer.js b/packages/react-ui/src/widgets/legend/LegendLayer.js index 091351281..d5ecdbde6 100644 --- a/packages/react-ui/src/widgets/legend/LegendLayer.js +++ b/packages/react-ui/src/widgets/legend/LegendLayer.js @@ -14,6 +14,18 @@ import useImperativeIntl from '../../hooks/useImperativeIntl'; const EMPTY_OBJ = {}; +/** + * @param {import('./LegendWidgetUI').LegendLayerData['legend']} legend + * @returns {boolean} + */ +function isLegendEmpty(legend) { + if (Array.isArray(legend)) { + return legend.every((l) => isLegendEmpty(l)); + } + + return !legend.select && !legend.type; +} + /** * Receives configuration options, send change events and renders a legend item * @param {object} props @@ -50,7 +62,7 @@ export default function LegendLayer({ const visible = layer.visible ?? true; const switchable = layer.switchable ?? true; const collapsed = layer.collapsed ?? false; - const collapsible = layer.collapsible ?? true; + const collapsible = (layer.collapsible ?? true) && !isLegendEmpty(layer.legend); const opacity = layer.opacity ?? 1; const showOpacityControl = layer.showOpacityControl ?? true; const isExpanded = visible && !collapsed; @@ -170,8 +182,13 @@ export default function LegendLayer({ ))} </LayerVariablesList> {helperText && ( - <Typography variant='caption' color='textSecondary' component='p' sx={{ p: 2 }}> - {helperText} + <Typography + variant='caption' + color='textSecondary' + component='div' + sx={{ p: 2 }} + > + <div dangerouslySetInnerHTML={{ __html: helperText }}></div> </Typography> )} </Collapse> diff --git a/packages/react-ui/src/widgets/legend/LegendLayerVariable.js b/packages/react-ui/src/widgets/legend/LegendLayerVariable.js index a944a229b..afa3cc04b 100644 --- a/packages/react-ui/src/widgets/legend/LegendLayerVariable.js +++ b/packages/react-ui/src/widgets/legend/LegendLayerVariable.js @@ -26,7 +26,7 @@ function LegendUnknown({ legend }) { const intl = useIntl(); const intlConfig = useImperativeIntl(intl); - if (legend.select) { + if (legend.select || !legend.type) { return null; } diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts index fa9aa5051..d937305f3 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts @@ -40,7 +40,7 @@ export type LegendLayerData = { helperText?: React.ReactNode; // note to show below all legend items minZoom?: number; // min zoom at which layer is displayed maxZoom?: number; // max zoom at which layer is displayed - legend?: LegendLayerVariableData | LegendLayerVariableData[]; + legend: LegendLayerVariableData | LegendLayerVariableData[]; }; export type LegendLayerVariableBase = { diff --git a/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js b/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js index 1a2219978..fd1c0ffe4 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js @@ -108,7 +108,10 @@ const LegendNotFoundTemplate = () => { { id: 0, title: 'Single Layer', - visible: true + visible: true, + legend: { + type: 'unknown' + } } ]; return <Template layers={layers} />; @@ -312,7 +315,8 @@ const LegendNoChildrenTemplate = () => { { id: 0, title: 'Single Layer', - visible: true + visible: true, + legend: {} } ]; return <Template layers={layers} />; diff --git a/packages/react-widgets/src/widgets/LegendWidget.js b/packages/react-widgets/src/widgets/LegendWidget.js index 31fbc676f..19103ecac 100644 --- a/packages/react-widgets/src/widgets/LegendWidget.js +++ b/packages/react-widgets/src/widgets/LegendWidget.js @@ -21,8 +21,9 @@ function LegendWidget({ customLegendTypes, initialCollapsed, layerOrder = [], ti sortLayers( Object.values(state.carto.layers).filter((layer) => !!layer.legend), layerOrder - ) + ).filter((l) => !!l.legend) ); + const [collapsed, setCollapsed] = useState(initialCollapsed); const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm')); const { zoom, maxZoom, minZoom } = useSelector((state) => state.carto.viewState); From 9b41b6da82f4311667de7eb9c62d6e21e2b5b098 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Mon, 12 Feb 2024 12:09:35 +0100 Subject: [PATCH 50/66] add back null check --- packages/react-ui/src/widgets/legend/LegendLayer.js | 4 ++++ packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/react-ui/src/widgets/legend/LegendLayer.js b/packages/react-ui/src/widgets/legend/LegendLayer.js index d5ecdbde6..3220c9f19 100644 --- a/packages/react-ui/src/widgets/legend/LegendLayer.js +++ b/packages/react-ui/src/widgets/legend/LegendLayer.js @@ -19,6 +19,10 @@ const EMPTY_OBJ = {}; * @returns {boolean} */ function isLegendEmpty(legend) { + if (!legend) { + return true; + } + if (Array.isArray(legend)) { return legend.every((l) => isLegendEmpty(l)); } diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts index d937305f3..fa9aa5051 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts @@ -40,7 +40,7 @@ export type LegendLayerData = { helperText?: React.ReactNode; // note to show below all legend items minZoom?: number; // min zoom at which layer is displayed maxZoom?: number; // max zoom at which layer is displayed - legend: LegendLayerVariableData | LegendLayerVariableData[]; + legend?: LegendLayerVariableData | LegendLayerVariableData[]; }; export type LegendLayerVariableBase = { From ce6fb178266f227134a31299eecb5d315b763e56 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Mon, 12 Feb 2024 12:19:43 +0100 Subject: [PATCH 51/66] fix hidden layer case --- .../__tests__/widgets/LegendWidgetUI.test.js | 23 +++++++++++++------ .../src/widgets/legend/LegendLayer.js | 4 ++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js b/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js index 76683f29f..ddd535249 100644 --- a/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js +++ b/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js @@ -109,14 +109,23 @@ describe('LegendWidgetUI', () => { }); test('layer with no legend is not shown on widget', () => { - const layer = { - id: 'test-layer-no-legend', - title: 'Test layer no legend', - visible: true - }; - render(<Widget layers={[layer]}></Widget>); + const layers = [ + { + id: 'test-layer-no-legend', + title: 'Test layer hidden', + visible: true + }, + { + id: 'test-layer-no-legend-2', + title: 'Test layer shown', + visible: true, + legend: {} + } + ]; + render(<Widget layers={layers}></Widget>); expect(screen.queryByText('Layers')).toBeInTheDocument(); - expect(screen.queryByText('Test layer no legend')).not.toBeInTheDocument(); + expect(screen.queryByText('Test layer hidden')).not.toBeInTheDocument(); + expect(screen.queryByText('Test layer shown')).toBeInTheDocument(); }); test('Category legend', () => { diff --git a/packages/react-ui/src/widgets/legend/LegendLayer.js b/packages/react-ui/src/widgets/legend/LegendLayer.js index 3220c9f19..1dc048420 100644 --- a/packages/react-ui/src/widgets/legend/LegendLayer.js +++ b/packages/react-ui/src/widgets/legend/LegendLayer.js @@ -93,6 +93,10 @@ export default function LegendLayer({ return Array.isArray(layer.legend) ? layer.legend : [layer.legend]; }, [layer.legend]); + if (!layer.legend) { + return null; + } + return ( <Box data-testid='legend-layer' From 4a4812a27d192a841e178b1ca8e4bc742a00493b Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Tue, 13 Feb 2024 10:13:36 +0100 Subject: [PATCH 52/66] add sx prop to types --- packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts index fa9aa5051..01aaf558f 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.d.ts @@ -1,3 +1,5 @@ +import { SxProps } from '@mui/material'; +import { Theme } from '@mui/system'; import type React from 'react' export enum LEGEND_TYPES { @@ -23,6 +25,7 @@ export type LegendWidgetUIProps = { minZoom?: number currentZoom?: number isMobile?: boolean + sx?: SxProps<Theme> } declare const LegendWidgetUI: (props: LegendWidgetUIProps) => React.ReactNode; From 671536240ad230d084551a6f0ed877d2e41fa1f7 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 7 Mar 2024 10:50:55 +0100 Subject: [PATCH 53/66] fix isSingle check --- packages/react-ui/src/widgets/legend/LegendWidgetUI.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/legend/LegendWidgetUI.js index ba3f23263..458a0074c 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.js @@ -70,7 +70,7 @@ function LegendWidgetUI({ </Tooltip> ); - if (isSingle) { + if (isSingle && !isMobile) { return ( <LegendRoot sx={sx} elevation={3} collapsed={collapsed || isMobile}> <LegendContent> From 0d4f31a39fb1e7d05e0c29e3de0e1ac4a42ba90e Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 7 Mar 2024 11:23:23 +0100 Subject: [PATCH 54/66] fix root style --- packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js b/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js index 1330e0f46..e697f7d1c 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js @@ -6,9 +6,8 @@ export const LEGEND_WIDTH = 240; export const LegendRoot = styled(Paper, { shouldForwardProp: (prop) => !['collapsed'].includes(prop) })(({ theme, collapsed }) => ({ - width: collapsed ? undefined : LEGEND_WIDTH, + width: collapsed ? 'min-content' : LEGEND_WIDTH, background: theme.palette.background.paper, - position: 'absolute', maxHeight: 'calc(100% - 120px)', display: 'flex', flexDirection: 'column' From 3bfaabd313d53e8a366e0c276c9e3d4cda4fcf3b Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 7 Mar 2024 12:07:31 +0100 Subject: [PATCH 55/66] fix number input styles for webkit --- .../react-ui/src/widgets/legend/LegendWidgetUI.styles.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js b/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js index e697f7d1c..a47abdd09 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js @@ -47,7 +47,12 @@ export const StyledOpacityControl = styled('div')(({ theme }) => ({ export const OpacityTextField = styled(TextField)(({ theme }) => ({ display: 'flex', width: theme.spacing(7.5), - flexShrink: 0 + flexShrink: 0, + 'input[type=number]::-webkit-inner-spin-button, input[type=number]::-webkit-outer-spin-button': + { + '-webkit-appearance': 'none', + margin: 0 + } })); export const LayerVariablesList = styled('ul', { From 5df38ffb039b988c66c2cd3cb30b3a9cc09cc5d4 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 7 Mar 2024 12:11:49 +0100 Subject: [PATCH 56/66] fix legendRamp --- .../react-ui/src/widgets/legend/legend-types/LegendRamp.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-ui/src/widgets/legend/legend-types/LegendRamp.js b/packages/react-ui/src/widgets/legend/legend-types/LegendRamp.js index fd65c0296..b1c5ae117 100644 --- a/packages/react-ui/src/widgets/legend/legend-types/LegendRamp.js +++ b/packages/react-ui/src/widgets/legend/legend-types/LegendRamp.js @@ -61,10 +61,10 @@ function LegendRamp({ isContinuous = false, legend }) { </Box> <Box sx={{ display: 'flex', justifyContent: 'space-between' }}> <Typography variant='overlineDelicate' color='textSecondary'> - {maxLabel} + {minLabel} </Typography> <Typography variant='overlineDelicate' color='textSecondary'> - {minLabel} + {maxLabel} </Typography> </Box> </> From 7c7249f9b9a778356ddc9ab5a8382e6ca4a82f11 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 7 Mar 2024 13:07:21 +0100 Subject: [PATCH 57/66] fix categories legend --- packages/react-ui/src/localization/en.js | 3 +- .../legend/legend-types/LegendCategories.js | 60 +++++++++++++------ .../widgetsUI/LegendWidgetUI.stories.js | 4 +- 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/packages/react-ui/src/localization/en.js b/packages/react-ui/src/localization/en.js index b837d4a07..73f83ea46 100644 --- a/packages/react-ui/src/localization/en.js +++ b/packages/react-ui/src/localization/en.js @@ -58,7 +58,8 @@ const locales = { color: 'Color based on' }, max: 'Max', - min: 'Min' + min: 'Min', + maxCategories: 'Legend limited to {n} categories' }, range: { clear: 'Clear', diff --git a/packages/react-ui/src/widgets/legend/legend-types/LegendCategories.js b/packages/react-ui/src/widgets/legend/legend-types/LegendCategories.js index d3a929886..513651bb3 100644 --- a/packages/react-ui/src/widgets/legend/legend-types/LegendCategories.js +++ b/packages/react-ui/src/widgets/legend/legend-types/LegendCategories.js @@ -1,9 +1,13 @@ import React from 'react'; -import { Box, styled } from '@mui/material'; +import { Box, Typography, styled } from '@mui/material'; import { getPalette } from '../../../utils/palette'; import PropTypes from 'prop-types'; import LegendLayerTitle from '../LegendLayerTitle'; import { LegendVariableList } from '../LegendWidgetUI.styles'; +import useImperativeIntl from '../../../hooks/useImperativeIntl'; +import { useIntl } from 'react-intl'; + +const MAX_CATEGORIES = 20; /** * @param {object} props @@ -20,24 +24,46 @@ function LegendCategories({ legend }) { } = legend; const palette = getPalette(colors, labels.length); + const showHelperText = labels.length > MAX_CATEGORIES; + const intl = useIntl(); + const intlConfig = useImperativeIntl(intl); return ( - <LegendVariableList data-testid='categories-legend'> - {labels.map((label, idx) => ( - <LegendCategoriesRow - key={label + idx} - label={label} - color={palette[idx]} - icon={ - customMarkers && Array.isArray(customMarkers) - ? customMarkers[idx] - : customMarkers - } - maskedIcon={maskedMarkers} - isStrokeColor={isStrokeColor} - /> - ))} - </LegendVariableList> + <> + <LegendVariableList data-testid='categories-legend'> + {labels.slice(0, MAX_CATEGORIES).map((label, idx) => ( + <LegendCategoriesRow + key={label + idx} + label={label} + color={palette[idx % palette.length]} + icon={ + customMarkers && Array.isArray(customMarkers) + ? customMarkers[idx] + : customMarkers + } + maskedIcon={maskedMarkers} + isStrokeColor={isStrokeColor} + /> + ))} + </LegendVariableList> + {showHelperText && ( + <Typography + variant='caption' + color='textSecondary' + component='div' + sx={{ py: 2 }} + > + {intlConfig.formatMessage( + { + id: 'c4r.widgets.legend.maxCategories' + }, + { + n: MAX_CATEGORIES + } + )} + </Typography> + )} + </> ); } diff --git a/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js b/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js index fd1c0ffe4..a9743bc00 100644 --- a/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js +++ b/packages/react-ui/storybook/stories/widgetsUI/LegendWidgetUI.stories.js @@ -201,8 +201,8 @@ const LegendMultiTemplateCollapsed = () => { const categoryLegend = { type: 'category', note: 'lorem', - colors: ['#000', '#00F', '#0F0'], - labels: ['Category 1', 'Category 2', 'Category 3'] + colors: 'RedOr', //['#000', '#00F', '#0F0'], + labels: Array.from({ length: 30 }, (_, i) => `Category ${i + 1}`) }; const LegendCategoriesTemplate = () => { From 0e9a1dcfbbfd488cde12a9e03869340c010ea33c Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 7 Mar 2024 13:44:22 +0100 Subject: [PATCH 58/66] change opacity show condition --- packages/react-ui/src/widgets/legend/LegendLayer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-ui/src/widgets/legend/LegendLayer.js b/packages/react-ui/src/widgets/legend/LegendLayer.js index 1dc048420..71b82584f 100644 --- a/packages/react-ui/src/widgets/legend/LegendLayer.js +++ b/packages/react-ui/src/widgets/legend/LegendLayer.js @@ -140,7 +140,7 @@ export default function LegendLayer({ </Tooltip> )} </Box> - {showOpacityControl && visible && ( + {showOpacityControl && visible && !collapsed && ( <LegendOpacityControl menuRef={menuAnchorRef} open={opacityOpen} From 8cc860a925e591a9e834f2853bb8cde4492545d3 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Fri, 8 Mar 2024 11:42:03 +0100 Subject: [PATCH 59/66] fix list accesibility --- packages/react-ui/src/widgets/legend/LegendLayerVariable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-ui/src/widgets/legend/LegendLayerVariable.js b/packages/react-ui/src/widgets/legend/LegendLayerVariable.js index afa3cc04b..d75f1be28 100644 --- a/packages/react-ui/src/widgets/legend/LegendLayerVariable.js +++ b/packages/react-ui/src/widgets/legend/LegendLayerVariable.js @@ -76,7 +76,7 @@ export default function LegendLayerVariable({ const selectOptions = legend.select?.options || []; return ( - <Box data-testid='legend-layer-variable' px={2}> + <Box component='li' data-testid='legend-layer-variable' px={2}> {legend.attr ? ( <Box pb={1}> <Typography From 6acf3e2a28be193e5cc2976509f09e9161908f86 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Fri, 8 Mar 2024 11:46:13 +0100 Subject: [PATCH 60/66] add name to legend select --- packages/react-ui/src/widgets/legend/LegendLayerVariable.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-ui/src/widgets/legend/LegendLayerVariable.js b/packages/react-ui/src/widgets/legend/LegendLayerVariable.js index d75f1be28..18f4ba1aa 100644 --- a/packages/react-ui/src/widgets/legend/LegendLayerVariable.js +++ b/packages/react-ui/src/widgets/legend/LegendLayerVariable.js @@ -98,6 +98,8 @@ export default function LegendLayerVariable({ {legend.select.label} </Typography> <Select + data-testid='legend-layer-variable-select' + name='legend-select' value={legend.select.value} renderValue={(value) => selectOptions.find((option) => option.value === value)?.label || value From 7b0575c59631021f504fe5e04c4dcf492794dd38 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Mon, 11 Mar 2024 11:58:48 +0100 Subject: [PATCH 61/66] remove single layer case --- .../src/widgets/legend/LegendWidgetUI.js | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.js b/packages/react-ui/src/widgets/legend/LegendWidgetUI.js index 458a0074c..4e783dae0 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.js +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.js @@ -48,7 +48,6 @@ function LegendWidgetUI({ } = {}) { const intl = useIntl(); const intlConfig = useImperativeIntl(intl); - const isSingle = layers.length === 1; const legendToggleHeader = ( <LegendToggleHeader collapsed={collapsed}> @@ -70,26 +69,6 @@ function LegendWidgetUI({ </Tooltip> ); - if (isSingle && !isMobile) { - return ( - <LegendRoot sx={sx} elevation={3} collapsed={collapsed || isMobile}> - <LegendContent> - <LegendLayer - layer={layers[0]} - onChangeCollapsed={onChangeLegendRowCollapsed} - onChangeOpacity={onChangeOpacity} - onChangeVisibility={onChangeVisibility} - onChangeSelection={onChangeSelection} - maxZoom={maxZoom} - minZoom={minZoom} - currentZoom={currentZoom} - customLegendTypes={customLegendTypes} - /> - </LegendContent> - </LegendRoot> - ); - } - return ( <LegendRoot sx={sx} elevation={3} collapsed={collapsed || isMobile}> {isMobile ? ( From 2037f0446e2e9022a7683e82c131e99a2e422677 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Tue, 12 Mar 2024 12:29:36 +0100 Subject: [PATCH 62/66] add id translations from @gandeszahara --- packages/react-ui/src/localization/id.js | 49 ++++++++++++------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/packages/react-ui/src/localization/id.js b/packages/react-ui/src/localization/id.js index b01d1e6a2..39fca16f3 100644 --- a/packages/react-ui/src/localization/id.js +++ b/packages/react-ui/src/localization/id.js @@ -33,33 +33,34 @@ const locales = { clear: 'Bersihkan' }, legend: { - by: 'By {attr}', - layerOptions: 'Layer options', - hide: 'Hide', - show: 'Show', + by: 'Berdasarkan {attr}', + layerOptions: 'Opsi Layer', + hide: 'Sembunyikan', + show: 'Tampilkan', layer: 'layer', - opacity: 'Opacity', - hideLayer: 'Hide layer', - showLayer: 'Show layer', - open: 'Open legend', - close: 'Close', - collapse: 'Collapse layer', - expand: 'Expand layer', - zoomLevel: 'Zoom level', - zoomLevelTooltip: 'This layer is only visible at certain zoom levels', - lowerThan: 'lower than', - greaterThan: 'greater than', - and: 'and', - zoomNote: 'Note: this layer will display at zoom levels', - notSupported: 'is not a known legend type', + opacity: 'Opasitas', + hideLayer: 'Sembunyikan layer', + showLayer: 'Tampilkan layer', + open: 'Buka legenda', + close: 'Tutup', + collapse: 'Ciutkan layer', + expand: 'Perluas layer', + zoomLevel: 'Tingkat Zoom', + zoomLevelTooltip: 'Layer ini hanya terlihat pada tingkat zoom tertentu', + lowerThan: 'lebih rendah dari', + greaterThan: 'lebih tinggi dari', + and: 'dan', + zoomNote: 'Catatan: layer ini akan ditampilkan pada tingkat zoom', + notSupported: 'bukan jenis legenda yang dikenal', subtitles: { - proportion: 'Radius range by', - icon: 'Icon based on', - strokeColor: 'Stroke color based on', - color: 'Color based on' + proportion: 'Rentang radius berdasarkan', + icon: 'Ikon berdasarkan', + strokeColor: 'Warna garis berdasarkan', + color: 'Warna berdasarkan' }, - max: 'Max', - min: 'Min' + max: 'Maks', + min: 'Min', + maxCategories: 'Legenda terbatas pada {n} kategori' }, range: { clear: 'Bersihkan', From 3c12b7a6280136cfbf3d22336a0ab3bd4470dde3 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Tue, 12 Mar 2024 12:30:43 +0100 Subject: [PATCH 63/66] remove unused keys --- packages/react-ui/src/localization/en.js | 1 - packages/react-ui/src/localization/es.js | 1 - packages/react-ui/src/localization/id.js | 1 - 3 files changed, 3 deletions(-) diff --git a/packages/react-ui/src/localization/en.js b/packages/react-ui/src/localization/en.js index 73f83ea46..ceea1b0d8 100644 --- a/packages/react-ui/src/localization/en.js +++ b/packages/react-ui/src/localization/en.js @@ -32,7 +32,6 @@ const locales = { clear: 'Clear' }, legend: { - by: 'By {attr}', layerOptions: 'Layer options', hide: 'Hide', show: 'Show', diff --git a/packages/react-ui/src/localization/es.js b/packages/react-ui/src/localization/es.js index 43e4cc505..e41421509 100644 --- a/packages/react-ui/src/localization/es.js +++ b/packages/react-ui/src/localization/es.js @@ -32,7 +32,6 @@ const locales = { clear: 'Limpiar' }, legend: { - by: 'Por {attr}', layerOptions: 'Opciones de capa', hide: 'Ocultar', show: 'Mostrar', diff --git a/packages/react-ui/src/localization/id.js b/packages/react-ui/src/localization/id.js index 39fca16f3..e432656a0 100644 --- a/packages/react-ui/src/localization/id.js +++ b/packages/react-ui/src/localization/id.js @@ -33,7 +33,6 @@ const locales = { clear: 'Bersihkan' }, legend: { - by: 'Berdasarkan {attr}', layerOptions: 'Opsi Layer', hide: 'Sembunyikan', show: 'Tampilkan', From 04e825c1c27cdae16e402aeb84ae13dd556979e7 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Tue, 12 Mar 2024 12:39:05 +0100 Subject: [PATCH 64/66] remove single legend test --- .../react-ui/__tests__/widgets/LegendWidgetUI.test.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js b/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js index ddd535249..f6059e11a 100644 --- a/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js +++ b/packages/react-ui/__tests__/widgets/LegendWidgetUI.test.js @@ -83,16 +83,6 @@ describe('LegendWidgetUI', () => { const Widget = (props) => <LegendWidgetUI {...props} />; - test('single legend', () => { - render(<Widget layers={[DATA[0]]}></Widget>); - // expanded legend toggle - expect(screen.queryByText('Layers')).not.toBeInTheDocument(); - // collapsed legend toggle - expect(screen.queryByLabelText('Layers')).not.toBeInTheDocument(); - // layer title - expect(screen.queryByTestId('categories-legend')).toBeInTheDocument(); - }); - test('multiple legends', () => { render(<Widget layers={DATA}></Widget>); expect(screen.queryByText('Layers')).toBeInTheDocument(); From 2d947d68a93ed688040ae879c85f64071081f5f9 Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Thu, 14 Mar 2024 18:12:47 +0100 Subject: [PATCH 65/66] fix selection ev on LegendWidget --- packages/react-widgets/src/widgets/LegendWidget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-widgets/src/widgets/LegendWidget.js b/packages/react-widgets/src/widgets/LegendWidget.js index 19103ecac..b13faef27 100644 --- a/packages/react-widgets/src/widgets/LegendWidget.js +++ b/packages/react-widgets/src/widgets/LegendWidget.js @@ -61,7 +61,7 @@ function LegendWidget({ customLegendTypes, initialCollapsed, layerOrder = [], ti const handleSelectionChange = ({ id, index, selection }) => { const layer = layers.find((layer) => layer.id === id); - const isMultiple = Array.isArray(selection); + const isMultiple = Array.isArray(layer.legend); const legend = isMultiple ? layer.legend : layer.legend[index]; const newLegend = { ...legend, From a12749bd8ac9edd54b2a1354c2e644b55b16456d Mon Sep 17 00:00:00 2001 From: "Juan D. Jara" <juanorigami@gmail.com> Date: Mon, 18 Mar 2024 15:55:10 +0100 Subject: [PATCH 66/66] refactor styles for sx, box and styled-components --- .../src/widgets/legend/LegendLayer.js | 34 +++++++------------ .../src/widgets/legend/LegendLayerTitle.js | 3 +- .../widgets/legend/LegendWidgetUI.styles.js | 25 +++++++++++--- .../legend/legend-types/LegendCategories.js | 2 +- .../widgets/legend/legend-types/LegendIcon.js | 19 ++++++----- .../legend/legend-types/LegendProportion.js | 4 +-- .../widgets/legend/legend-types/LegendRamp.js | 6 ++-- 7 files changed, 52 insertions(+), 41 deletions(-) diff --git a/packages/react-ui/src/widgets/legend/LegendLayer.js b/packages/react-ui/src/widgets/legend/LegendLayer.js index 71b82584f..2b035644f 100644 --- a/packages/react-ui/src/widgets/legend/LegendLayer.js +++ b/packages/react-ui/src/widgets/legend/LegendLayer.js @@ -1,16 +1,22 @@ import React, { useMemo, useRef, useState } from 'react'; import PropTypes from 'prop-types'; -import { Box, Collapse, IconButton, Tooltip, Typography } from '@mui/material'; +import { Collapse, IconButton, Tooltip } from '@mui/material'; import EyeIcon from '@mui/icons-material/VisibilityOutlined'; import EyeOffIcon from '@mui/icons-material/VisibilityOffOutlined'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; -import { LayerVariablesList, LegendItemHeader } from './LegendWidgetUI.styles'; +import { + LayerVariablesList, + LegendItemHeader, + LegendLayerTitleWrapper, + LegendLayerWrapper +} from './LegendWidgetUI.styles'; import LegendOpacityControl from './LegendOpacityControl'; import LegendLayerTitle from './LegendLayerTitle'; import LegendLayerVariable from './LegendLayerVariable'; import { useIntl } from 'react-intl'; import useImperativeIntl from '../../hooks/useImperativeIntl'; +import Typography from '../../components/atoms/Typography'; const EMPTY_OBJ = {}; @@ -98,16 +104,7 @@ export default function LegendLayer({ } return ( - <Box - data-testid='legend-layer' - aria-label={title} - component='section' - sx={{ - '&:not(:first-of-type)': { - borderTop: (theme) => `1px solid ${theme.palette.divider}` - } - }} - > + <LegendLayerWrapper data-testid='legend-layer' aria-label={title}> <LegendItemHeader ref={menuAnchorRef}> {collapsible && ( <IconButton @@ -121,7 +118,7 @@ export default function LegendLayer({ {collapseIcon} </IconButton> )} - <Box flexGrow={1} sx={{ minWidth: 0, flexShrink: 1 }}> + <LegendLayerTitleWrapper> <LegendLayerTitle visible={visible} title={title} /> {showZoomNote && ( <Tooltip @@ -139,7 +136,7 @@ export default function LegendLayer({ </Typography> </Tooltip> )} - </Box> + </LegendLayerTitleWrapper> {showOpacityControl && visible && !collapsed && ( <LegendOpacityControl menuRef={menuAnchorRef} @@ -190,17 +187,12 @@ export default function LegendLayer({ ))} </LayerVariablesList> {helperText && ( - <Typography - variant='caption' - color='textSecondary' - component='div' - sx={{ p: 2 }} - > + <Typography variant='caption' color='textSecondary' component='div' p={2}> <div dangerouslySetInnerHTML={{ __html: helperText }}></div> </Typography> )} </Collapse> - </Box> + </LegendLayerWrapper> ); } diff --git a/packages/react-ui/src/widgets/legend/LegendLayerTitle.js b/packages/react-ui/src/widgets/legend/LegendLayerTitle.js index ccd591ff0..a88b62fce 100644 --- a/packages/react-ui/src/widgets/legend/LegendLayerTitle.js +++ b/packages/react-ui/src/widgets/legend/LegendLayerTitle.js @@ -26,10 +26,9 @@ export default function LegendLayerTitle({ title, visible, typographyProps }) { color={visible ? 'textPrimary' : 'textSecondary'} variant='button' weight='medium' - lineHeight='20px' component='p' noWrap - sx={{ my: 0.25 }} + my={0.25} {...typographyProps} > {title} diff --git a/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js b/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js index a47abdd09..1e78933d4 100644 --- a/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js +++ b/packages/react-ui/src/widgets/legend/LegendWidgetUI.styles.js @@ -41,7 +41,7 @@ export const StyledOpacityControl = styled('div')(({ theme }) => ({ gap: theme.spacing(2), alignItems: 'center', padding: theme.spacing(1), - minWidth: LEGEND_WIDTH - 32 + minWidth: LEGEND_WIDTH - theme.spacingValue * 4 })); export const OpacityTextField = styled(TextField)(({ theme }) => ({ @@ -74,7 +74,12 @@ export const LegendVariableList = styled('ul')(({ theme }) => ({ flexDirection: 'column' })); -export const LegendIconWrapper = styled('div')(({ theme }) => ({ +export const LegendIconWrapper = styled('li')(() => ({ + display: 'flex', + alignItems: 'center' +})); + +export const LegendIconImageWrapper = styled('div')(({ theme }) => ({ marginRight: theme.spacing(1.5), width: ICON_SIZE_MEDIUM, height: ICON_SIZE_MEDIUM, @@ -86,10 +91,22 @@ export const LegendIconWrapper = styled('div')(({ theme }) => ({ export const LegendContent = styled(Box, { shouldForwardProp: (prop) => !['width'].includes(prop) -})(({ width }) => ({ +})(({ width, theme }) => ({ width, overflow: 'auto', - maxHeight: `calc(100% - 12px)` + maxHeight: `calc(100% - ${theme.spacing(1.5)})` +})); + +export const LegendLayerWrapper = styled('section')(({ theme }) => ({ + '&:not(:first-of-type)': { + borderTop: `1px solid ${theme.palette.divider}` + } +})); + +export const LegendLayerTitleWrapper = styled('div')(() => ({ + flexGrow: 1, + flexShrink: 1, + minWidth: 0 })); export const styles = {}; diff --git a/packages/react-ui/src/widgets/legend/legend-types/LegendCategories.js b/packages/react-ui/src/widgets/legend/legend-types/LegendCategories.js index 513651bb3..760840ba6 100644 --- a/packages/react-ui/src/widgets/legend/legend-types/LegendCategories.js +++ b/packages/react-ui/src/widgets/legend/legend-types/LegendCategories.js @@ -159,7 +159,7 @@ function LegendCategoriesRow({ label, isStrokeColor, color = '#000', icon, maske <LegendLayerTitle title={label} visible - typographyProps={{ variant: 'overlineDelicate' }} + typographyProps={{ variant: 'overline', my: 0.75 }} /> </Box> ); diff --git a/packages/react-ui/src/widgets/legend/legend-types/LegendIcon.js b/packages/react-ui/src/widgets/legend/legend-types/LegendIcon.js index e3b72c0ed..3a9317bbe 100644 --- a/packages/react-ui/src/widgets/legend/legend-types/LegendIcon.js +++ b/packages/react-ui/src/widgets/legend/legend-types/LegendIcon.js @@ -1,8 +1,11 @@ import React from 'react'; -import { Box } from '@mui/material'; import PropTypes from 'prop-types'; import { ICON_SIZE_MEDIUM } from '../../../theme/themeConstants'; -import { LegendIconWrapper, LegendVariableList } from '../LegendWidgetUI.styles'; +import { + LegendIconImageWrapper, + LegendIconWrapper, + LegendVariableList +} from '../LegendWidgetUI.styles'; import LegendLayerTitle from '../LegendLayerTitle'; /** @@ -15,16 +18,16 @@ function LegendIcon({ legend }) { return ( <LegendVariableList data-testid='icon-legend'> {labels.map((label, idx) => ( - <Box key={label} component='li' sx={{ display: 'flex', alignItems: 'center' }}> - <LegendIconWrapper> - <img src={icons[idx]} alt={label} width='autio' height={ICON_SIZE_MEDIUM} /> - </LegendIconWrapper> + <LegendIconWrapper key={label}> + <LegendIconImageWrapper> + <img src={icons[idx]} alt={label} width='auto' height={ICON_SIZE_MEDIUM} /> + </LegendIconImageWrapper> <LegendLayerTitle visible title={label} - typographyProps={{ variant: 'overlineDelicate' }} + typographyProps={{ variant: 'overline', my: 0.75 }} /> - </Box> + </LegendIconWrapper> ))} </LegendVariableList> ); diff --git a/packages/react-ui/src/widgets/legend/legend-types/LegendProportion.js b/packages/react-ui/src/widgets/legend/legend-types/LegendProportion.js index 5ddbeffb0..16b6fa8fd 100644 --- a/packages/react-ui/src/widgets/legend/legend-types/LegendProportion.js +++ b/packages/react-ui/src/widgets/legend/legend-types/LegendProportion.js @@ -19,8 +19,8 @@ const Circle = styled(Box, { const height = theme.spacing(sizes[index]); return { - border: `solid 1px ${theme.palette.grey[100]}`, - backgroundColor: theme.palette.grey[50], + border: `solid 1px ${theme.palette.divider}`, + backgroundColor: theme.palette.background.default, borderRadius: '50%', position: 'absolute', right: 0, diff --git a/packages/react-ui/src/widgets/legend/legend-types/LegendRamp.js b/packages/react-ui/src/widgets/legend/legend-types/LegendRamp.js index b1c5ae117..8f2f5a1bf 100644 --- a/packages/react-ui/src/widgets/legend/legend-types/LegendRamp.js +++ b/packages/react-ui/src/widgets/legend/legend-types/LegendRamp.js @@ -38,7 +38,7 @@ function LegendRamp({ isContinuous = false, legend }) { } return ( - <Box sx={{ py: 2 }} data-testid='ramp-legend'> + <Box py={2} data-testid='ramp-legend'> {error ? ( <Box maxWidth={240}> <Typography variant='overline'> @@ -47,7 +47,7 @@ function LegendRamp({ isContinuous = false, legend }) { </Box> ) : ( <> - <Box sx={{ display: 'flex', pb: 1 }}> + <Box display='flex' pb={1}> {isContinuous ? ( <StepsContinuous data-testid='step-continuous' palette={palette} /> ) : ( @@ -59,7 +59,7 @@ function LegendRamp({ isContinuous = false, legend }) { /> )} </Box> - <Box sx={{ display: 'flex', justifyContent: 'space-between' }}> + <Box display='flex' justifyContent='space-between'> <Typography variant='overlineDelicate' color='textSecondary'> {minLabel} </Typography>