-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New Legend Widget #837
Merged
Merged
New Legend Widget #837
Changes from 19 commits
Commits
Show all changes
69 commits
Select commit
Hold shift + click to select a range
ebe5239
WIP new legend
juandjara b345650
finish icon buttons with tooltips
juandjara c264e1c
legend helper text
juandjara 5250f55
extract legend width constant
juandjara 0499442
conditional overflow tooltip component
juandjara 07268b0
sticky backgrounds
juandjara 28af075
remove unnecesary prop passing in favor of unmountOnExit
juandjara 6f0b7c6
split new legend into multiple files
juandjara 4b78784
disable opacity button when layer is not visible
juandjara 9bb1d39
first step of multi-variable legend
juandjara c89a5b9
show opacity control only when visible
juandjara 94cd51b
add custom legend types config and improve multi legend support
juandjara f1d26f1
extract opacity component
juandjara f58780c
rename types
juandjara aed0d6b
rename legendItem to legendLayer and split components more
juandjara 0f29e5d
option selector WIP
juandjara a09426e
finish layer selector
juandjara 4e3b78d
translate visibility button
juandjara d84416a
fix import
juandjara b2491d7
translate more layers
juandjara d062ecc
translate all labels
juandjara dee933e
extract opacity icon
juandjara b1245ae
add more styles
juandjara eed96c2
legend attr subtitles
juandjara 45abf19
complete legend types typing
juandjara 87a9b2a
update individual legend styles
juandjara 1ccb75c
extract list styles
juandjara 674a06e
add min max configurable for proportion legend
juandjara d38a420
add default min max config for legend ramp
juandjara 17c7aa5
wip mobile menu
juandjara f5cbccb
update react imports
juandjara 53b05b6
fix the weirdest import bug ever
juandjara 746e09a
finish mobile config
juandjara 16491d3
fix typography import
juandjara f18e63f
refactor legend proportion
juandjara 885114b
use typography atom component
juandjara 45fa551
update jsdocs
juandjara 04aa1c8
update all legends finished, and tests passing
juandjara b2f357a
remove console log
juandjara a29a702
everything done on widget, storybooks and tests
juandjara a9de216
rename storybook
juandjara d80115f
rename all new-legend components to replace old legend folder
juandjara a0b047e
WIP move sx to styled components
juandjara 51f127e
more styled components
juandjara 8d44ee8
fix styled components
juandjara 49c4e4d
more styled components refactor
juandjara e9ccae4
remove unused param
juandjara 062481b
update default max zoom
juandjara 803cbe2
edge cases for empty and single layers
juandjara 9b41b6d
add back null check
juandjara ce6fb17
fix hidden layer case
juandjara 4a4812a
add sx prop to types
juandjara 6715362
fix isSingle check
juandjara 0d4f31a
fix root style
juandjara 3bfaabd
fix number input styles for webkit
juandjara 5df38ff
fix legendRamp
juandjara 7c7249f
fix categories legend
juandjara 0e9a1dc
change opacity show condition
juandjara 8cc860a
fix list accesibility
juandjara 6acf3e2
add name to legend select
juandjara 7b0575c
remove single layer case
juandjara 2037f04
add id translations from @gandeszahara
juandjara 3c12b7a
remove unused keys
juandjara 04e825c
remove single legend test
juandjara 2d947d6
fix selection ev on LegendWidget
juandjara a12749b
refactor styles for sx, box and styled-components
juandjara d4ebc64
replace mui typography with project category
juandjara e9089a0
fix legend categories story
juandjara 1bd89d4
Merge branch 'master' of github.com:CartoDB/carto-react into feature/…
padawannn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,69 @@ | ||
import type React from 'react' | ||
|
||
export enum LEGEND_TYPES { | ||
CATEGORY = 'category', | ||
ICON = 'icon', | ||
CONTINUOUS_RAMP = 'continuous_ramp', | ||
BINS = 'bins', | ||
PROPORTION = 'proportion', | ||
CUSTOM = 'custom' | ||
} | ||
|
||
export type LegendLayerData = { | ||
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?: LegendLayerVariableData | LegendLayerVariableData[]; | ||
}; | ||
|
||
export type LegendLayerVariableData = { | ||
type: LEGEND_TYPES; | ||
select: LegendSelectConfig | ||
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 LegendSelectConfig<T = unknown> = { | ||
label: string; | ||
value: T; | ||
options: { | ||
label: string; | ||
value: T; | ||
}[]; | ||
}; | ||
|
||
export type CustomLegendComponent = React.ComponentType<{ | ||
layer: LegendLayerData; | ||
legend: LegendLayerVariableData; | ||
}>; |
203 changes: 203 additions & 0 deletions
203
packages/react-ui/src/widgets/new-legend/LegendLayer.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
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'; | ||
import LegendLayerVariable from './LegendLayerVariable'; | ||
import { useIntl } from 'react-intl'; | ||
import useImperativeIntl from '../../hooks/useImperativeIntl'; | ||
|
||
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 {({ 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, 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. | ||
* @returns {React.ReactNode} | ||
*/ | ||
export default function LegendLayer({ | ||
customLegendTypes = EMPTY_OBJ, | ||
layer = EMPTY_OBJ, | ||
onChangeCollapsed, | ||
onChangeOpacity, | ||
onChangeVisibility, | ||
onChangeSelection, | ||
maxZoom, | ||
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; | ||
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 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 : ''; | ||
|
||
const legendLayerVariables = useMemo(() => { | ||
if (!layer.legend) { | ||
return []; | ||
} | ||
return Array.isArray(layer.legend) ? layer.legend : [layer.legend]; | ||
}, [layer.legend]); | ||
|
||
return ( | ||
<Box | ||
aria-label={title} | ||
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' | ||
aaranadev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
disabled={!visible} | ||
onClick={() => onChangeCollapsed({ id, collapsed: !collapsed })} | ||
> | ||
{collapseIcon} | ||
</IconButton> | ||
)} | ||
<Box flexGrow={1} sx={{ minWidth: 0, flexShrink: 1 }}> | ||
<LegendLayerTitle visible={visible} title={title} /> | ||
{showZoomNote && ( | ||
<Typography | ||
color={visible ? 'textPrimary' : 'textSecondary'} | ||
variant='caption' | ||
component='p' | ||
> | ||
Zoom level: {layer.minZoom} - {layer.maxZoom} | ||
aaranadev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</Typography> | ||
)} | ||
</Box> | ||
{showOpacityControl && visible && ( | ||
<LegendOpacityControl | ||
menuRef={menuAnchorRef} | ||
open={opacityOpen} | ||
toggleOpen={setOpacityOpen} | ||
opacity={opacity} | ||
onChange={(opacity) => onChangeOpacity({ id, opacity })} | ||
/> | ||
)} | ||
{switchable && ( | ||
<Tooltip | ||
title={intlConfig.formatMessage({ | ||
id: visible | ||
? 'c4r.widgets.legend.hideLayer' | ||
: 'c4r.widgets.legend.showLayer' | ||
})} | ||
> | ||
<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}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it necessary to unmountOnExit here? |
||
<Box pb={2} opacity={outsideCurrentZoom ? 0.5 : 1}> | ||
{legendLayerVariables.map((legend, index) => ( | ||
<LegendLayerVariable | ||
key={legend.type} | ||
legend={legend} | ||
layer={layer} | ||
customLegendTypes={customLegendTypes} | ||
onChangeSelection={(selection) => | ||
onChangeSelection({ id, index, selection }) | ||
} | ||
/> | ||
))} | ||
</Box> | ||
{helperText && ( | ||
<Typography | ||
variant='caption' | ||
color='textSecondary' | ||
component='p' | ||
sx={{ py: 2 }} | ||
> | ||
{helperText} | ||
</Typography> | ||
)} | ||
</Collapse> | ||
</Box> | ||
); | ||
} | ||
|
||
LegendLayer.propTypes = { | ||
customLegendTypes: PropTypes.object.isRequired, | ||
layer: PropTypes.object.isRequired, | ||
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 | ||
}; | ||
LegendLayer.defaultProps = { | ||
maxZoom: 21, | ||
minZoom: 0, | ||
currentZoom: 0 | ||
}; | ||
|
||
/** | ||
* @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}` : ''; | ||
aaranadev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
41 changes: 41 additions & 0 deletions
41
packages/react-ui/src/widgets/new-legend/LegendLayerTitle.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it still necessary visible? |
||
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>; | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review the imports