-
Notifications
You must be signed in to change notification settings - Fork 1
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
[Dashboard] Bloc zones réglementaires + Création du composant accordéon #1711
Changes from all commits
14b7947
1f2a515
bdab387
6a1fda5
596f7cf
7a24aab
90b4877
4cc4966
242528b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -74,4 +74,8 @@ html { | |
} | ||
} | ||
|
||
h2 { | ||
line-height: normal; | ||
} | ||
|
||
` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { useGetRegulatoryLayersQuery } from '@api/regulatoryLayersAPI' | ||
import { getRegulatoryFeature } from '@features/map/layers/Regulatory/regulatoryGeometryHelpers' | ||
import { getRegulatoryLayerStyle } from '@features/map/layers/styles/administrativeAndRegulatoryLayers.style' | ||
import { useAppSelector } from '@hooks/useAppSelector' | ||
import { Feature } from 'ol' | ||
import VectorLayer from 'ol/layer/Vector' | ||
import VectorSource from 'ol/source/Vector' | ||
import { useEffect, useRef } from 'react' | ||
|
||
import { Dashboard } from '../types' | ||
|
||
import type { BaseMapChildrenProps } from '@features/map/BaseMap' | ||
import type { VectorLayerWithName } from 'domain/types/layer' | ||
import type { Geometry } from 'ol/geom' | ||
|
||
export function DashboardLayer({ map }: BaseMapChildrenProps) { | ||
const displayDashboardLayer = useAppSelector(state => state.global.displayDashboardLayer) | ||
|
||
const activeDashboardId = useAppSelector(state => state.dashboard.activeDashboardId) | ||
|
||
const selectedRegulatoryAreaIds = useAppSelector(state => | ||
activeDashboardId ? state.dashboard.dashboards?.[activeDashboardId]?.[Dashboard.Block.REGULATORY_AREAS] : [] | ||
) | ||
|
||
const isLayerVisible = displayDashboardLayer | ||
|
||
const { data: regulatoryLayers } = useGetRegulatoryLayersQuery() | ||
|
||
const vectorSourceRef = useRef(new VectorSource()) as React.MutableRefObject<VectorSource<Feature<Geometry>>> | ||
const vectorLayerRef = useRef( | ||
new VectorLayer({ | ||
renderBuffer: 7, | ||
renderOrder: (a, b) => b.get('area') - a.get('area'), | ||
source: vectorSourceRef.current, | ||
style: getRegulatoryLayerStyle, | ||
updateWhileAnimating: true, | ||
updateWhileInteracting: true, | ||
zIndex: 1500 // TODO: create constants | ||
}) | ||
) as React.MutableRefObject<VectorLayerWithName> | ||
;(vectorLayerRef.current as VectorLayerWithName).name = 'DASHBOARD' // TODO: create constants | ||
|
||
useEffect(() => { | ||
if (map) { | ||
vectorSourceRef.current.clear(true) | ||
|
||
if (regulatoryLayers?.entities) { | ||
const features = (selectedRegulatoryAreaIds ?? []).reduce((feats: Feature[], layerId) => { | ||
const layer = regulatoryLayers.entities[layerId] | ||
if (layer && layer?.geom && layer?.geom?.coordinates.length > 0) { | ||
const feature = getRegulatoryFeature({ code: 'DASHBOARD', layer }) | ||
|
||
feats.push(feature) | ||
} | ||
|
||
return feats | ||
}, []) | ||
|
||
vectorSourceRef.current.addFeatures(features) | ||
} | ||
} | ||
}, [map, regulatoryLayers, selectedRegulatoryAreaIds]) | ||
|
||
useEffect(() => { | ||
map.getLayers().push(vectorLayerRef.current) | ||
|
||
return () => { | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
map.removeLayer(vectorLayerRef.current) | ||
} | ||
}, [map]) | ||
|
||
useEffect(() => { | ||
vectorLayerRef.current?.setVisible(isLayerVisible) | ||
}, [isLayerVisible]) | ||
|
||
return null | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { Accent, Icon, IconButton } from '@mtes-mct/monitor-ui' | ||
import styled from 'styled-components' | ||
|
||
type AccordionProps = { | ||
children: React.ReactNode | ||
headerButton?: React.ReactNode | ||
isExpanded: boolean | ||
setExpandedAccordion: () => void | ||
title: string | ||
} | ||
|
||
export function Accordion({ children, headerButton, isExpanded, setExpandedAccordion, title }: AccordionProps) { | ||
return ( | ||
<AccordionContainer $withCursor={!headerButton}> | ||
<AccordionHeader | ||
aria-controls={`${title}-accordion`} | ||
aria-expanded={isExpanded} | ||
onClick={!headerButton ? setExpandedAccordion : undefined} | ||
> | ||
<TitleContainer> | ||
<Title>{title}</Title> | ||
{headerButton} | ||
</TitleContainer> | ||
<StyledIconButton | ||
$isExpanded={isExpanded} | ||
accent={Accent.TERTIARY} | ||
Icon={Icon.Chevron} | ||
onClick={setExpandedAccordion} | ||
/> | ||
</AccordionHeader> | ||
<HeaderSeparator /> | ||
<AccordionContent $isExpanded={isExpanded} id={`${title}-accordion`}> | ||
{children} | ||
</AccordionContent> | ||
</AccordionContainer> | ||
) | ||
} | ||
|
||
const AccordionContainer = styled.div<{ $withCursor: boolean }>` | ||
box-shadow: 0px 3px 6px #70778540; | ||
cursor: ${({ $withCursor }) => ($withCursor ? 'pointer' : 'default')}; | ||
` | ||
const StyledIconButton = styled(IconButton)<{ $isExpanded: boolean }>` | ||
transform: ${({ $isExpanded }) => ($isExpanded ? 'rotate(180deg)' : 'rotate(0deg)')}; | ||
transition: transform 0.3s; | ||
` | ||
const AccordionHeader = styled.header` | ||
display: flex; | ||
justify-content: space-between; | ||
padding: 24px; | ||
` | ||
const TitleContainer = styled.div` | ||
align-items: center; | ||
display: flex; | ||
gap: 16px; | ||
` | ||
const Title = styled.h2` | ||
font-size: 16px; | ||
font-weight: 500; | ||
` | ||
|
||
const HeaderSeparator = styled.div` | ||
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. suggestion: si tu met la border-bottom sur la div du container ca fait le meme taf non ? 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. Le header a un padding alors que le trait fait toute la longueur c'est pour ça |
||
border-bottom: 2px solid ${p => p.theme.color.gainsboro}; | ||
padding: -24px; | ||
` | ||
const AccordionContent = styled.div<{ $isExpanded: boolean }>` | ||
display: flex; | ||
flex-direction: column; | ||
max-height: ${({ $isExpanded }) => ($isExpanded ? '100vh' : '0px')}; | ||
overflow-x: hidden; | ||
transition: 0.5s max-height; | ||
` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import { dashboardActions } from '@features/Dashboard/slice' | ||
import { Dashboard } from '@features/Dashboard/types' | ||
import { LayerLegend } from '@features/layersSelector/utils/LayerLegend.style' | ||
import { LayerSelector } from '@features/layersSelector/utils/LayerSelector.style' | ||
import { useAppSelector } from '@hooks/useAppSelector' | ||
import { Accent, Icon, IconButton, THEME } from '@mtes-mct/monitor-ui' | ||
import { transformExtent } from 'ol/proj' | ||
import Projection from 'ol/proj/Projection' | ||
import { createRef } from 'react' | ||
import styled from 'styled-components' | ||
|
||
import { useGetRegulatoryLayersQuery } from '../../../../../api/regulatoryLayersAPI' | ||
import { MonitorEnvLayers } from '../../../../../domain/entities/layers/constants' | ||
import { OPENLAYERS_PROJECTION, WSG84_PROJECTION } from '../../../../../domain/entities/map/constants' | ||
import { setFitToExtent } from '../../../../../domain/shared_slices/Map' | ||
import { useAppDispatch } from '../../../../../hooks/useAppDispatch' | ||
|
||
type RegulatoryLayerProps = { | ||
dashboardId: number | ||
isSelected: boolean | ||
layerId: number | ||
} | ||
|
||
export function Layer({ dashboardId, isSelected, layerId }: RegulatoryLayerProps) { | ||
const dispatch = useAppDispatch() | ||
const ref = createRef<HTMLSpanElement>() | ||
|
||
const selectedRegulatoryAreas = useAppSelector( | ||
state => state.dashboard.dashboards?.[dashboardId]?.[Dashboard.Block.REGULATORY_AREAS] | ||
) | ||
|
||
const isZoneSelected = selectedRegulatoryAreas?.includes(layerId) | ||
const { layer } = useGetRegulatoryLayersQuery(undefined, { | ||
selectFromResult: result => ({ | ||
layer: result?.currentData?.entities[layerId] | ||
}) | ||
}) | ||
|
||
const handleSelectZone = e => { | ||
e.stopPropagation() | ||
|
||
const payload = { itemIds: [layerId], type: Dashboard.Block.REGULATORY_AREAS } | ||
if (isZoneSelected) { | ||
dispatch(dashboardActions.removeItems(payload)) | ||
} else { | ||
dispatch(dashboardActions.addItems(payload)) | ||
if (!layer?.bbox) { | ||
return | ||
} | ||
const extent = transformExtent( | ||
layer?.bbox, | ||
new Projection({ code: WSG84_PROJECTION }), | ||
new Projection({ code: OPENLAYERS_PROJECTION }) | ||
) | ||
dispatch(setFitToExtent(extent)) | ||
} | ||
} | ||
|
||
const removeZone = e => { | ||
e.stopPropagation() | ||
dispatch(dashboardActions.removeItems({ itemIds: [layerId], type: Dashboard.Block.REGULATORY_AREAS })) | ||
} | ||
|
||
const toggleZoneMetadata = () => { | ||
dispatch(dashboardActions.setDashboardPanel({ id: layerId, type: Dashboard.Block.REGULATORY_AREAS })) | ||
} | ||
|
||
return ( | ||
<StyledLayer ref={ref} $isSelected={isSelected} onClick={toggleZoneMetadata}> | ||
<LayerLegend | ||
layerType={MonitorEnvLayers.REGULATORY_ENV} | ||
legendKey={layer?.entity_name ?? 'aucun'} | ||
type={layer?.thematique ?? 'aucun'} | ||
/> | ||
<LayerSelector.Name $withLargeWidth title={layer?.entity_name}> | ||
{layer?.entity_name ?? 'AUCUN NOM'} | ||
</LayerSelector.Name> | ||
|
||
<LayerSelector.IconGroup> | ||
{isSelected ? ( | ||
<IconButton | ||
accent={Accent.TERTIARY} | ||
aria-label="Supprimer la zone" | ||
color={THEME.color.slateGray} | ||
Icon={Icon.Close} | ||
onClick={removeZone} | ||
title="Supprimer la/ zone" | ||
/> | ||
) : ( | ||
<IconButton | ||
accent={Accent.TERTIARY} | ||
aria-label="Sélectionner la zone" | ||
color={isZoneSelected ? THEME.color.blueGray : THEME.color.slateGray} | ||
data-cy="regulatory-zone-check" | ||
Icon={isZoneSelected ? Icon.PinFilled : Icon.Pin} | ||
onClick={handleSelectZone} | ||
/> | ||
)} | ||
</LayerSelector.IconGroup> | ||
</StyledLayer> | ||
) | ||
} | ||
|
||
const StyledLayer = styled(LayerSelector.Layer)<{ $isSelected: boolean }>` | ||
background-color: ${p => p.theme.color.white}; | ||
padding-left: 24px; | ||
padding-right: 24px; | ||
${p => | ||
p.$isSelected && | ||
` | ||
padding-left: 20px; | ||
padding-right: 20px; | ||
margin-left: 4px; | ||
margin-right: 4px; | ||
`} | ||
` |
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.
question: pourquoi mettre tout le container avec le pointer ?
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.
c'était une demande de Xavier de pouvoir cliquer sur tout le container pour ouvrir l'accordéon. J'ai fait sur tout le container quand il n'y a pas d'autre icône et quand il y a une icône (cf bloc signalement) , je l'ai mis seulement sur le chevron.
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.
suggestion: comme ce n'est pas un bouton mais une div cliquable. Ce serait bien d'avoir un
aria-control
qui réfère ce qu'il se ferme ou pas ainsi qu'unaria-expanded