diff --git a/examples/nuxt-app/app.config.ts b/examples/nuxt-app/app.config.ts index 2aa9ef7b0e..7b7491c20a 100644 --- a/examples/nuxt-app/app.config.ts +++ b/examples/nuxt-app/app.config.ts @@ -46,16 +46,25 @@ export default defineAppConfig({ }, mapPinStyleFn: { vsbaPinIcons: (feature) => { - const projectType = feature ? feature['field_mappintype_name'][0] : '' + const projectType = + feature && feature['field_mappintype_name'] + ? feature['field_mappintype_name'][0] + : '' switch (projectType) { - case 'Early childhood': - return '#7c1792' + case 'New school': + return '#8A2A2B' case 'School upgrade': return '#df4809' - case 'New school': - return '#ff941a' + case 'Planning project': + return '#FF9E1B' + case 'Early childhood': + return '#87189D' + case 'Tech school': + return '#00B2A9' + case 'Non-government grant': + return '#71C5E8' default: - return '#a13434' + return '#333333' } } }, diff --git a/examples/nuxt-app/components/global/VSBAMapPopupContent.vue b/examples/nuxt-app/components/global/VSBAMapPopupContent.vue index 3a14264d41..996c9ec4a8 100644 --- a/examples/nuxt-app/components/global/VSBAMapPopupContent.vue +++ b/examples/nuxt-app/components/global/VSBAMapPopupContent.vue @@ -1,42 +1,52 @@ diff --git a/examples/nuxt-app/components/global/VSBAProjectAreaLayer.vue b/examples/nuxt-app/components/global/VSBAProjectAreaLayer.vue index 000e46d81a..e290915227 100644 --- a/examples/nuxt-app/components/global/VSBAProjectAreaLayer.vue +++ b/examples/nuxt-app/components/global/VSBAProjectAreaLayer.vue @@ -27,10 +27,15 @@ const props = withDefaults(defineProps(), { areaDataKey: 'field_postcode' }) +const areas = computed(() => { + const matches = props.results.filter((itm) => { + return !itm.lat && itm[props.areaDataKey] + }) + return matches +}) + const mappedAreas = computed(() => { - return props.results - .filter((itm) => !itm.lat && itm[props.areaDataKey]) - .map((area) => `'${area[props.areaDataKey]}'`) + return areas.value.map((area) => `'${area[props.areaDataKey]}'`) }) const shapeFormat = new GeoJSON() @@ -49,16 +54,16 @@ const defaultStyleFn = new Style({ }), stroke: new Stroke({ color: props.lineColor, - width: 1 + width: 2 }) }) const mouseOverStyleFn = new Style({ fill: new Fill({ - color: [26, 26, 26, 0.3] + color: [26, 26, 26, 0.15] }), stroke: new Stroke({ color: props.lineColor, - width: 1 + width: 2 }) }) @@ -97,21 +102,41 @@ onMounted(async () => { } map.on('singleclick', function (evt) { + const clickedFeatures = [] + // We need to keep track of features that are clicked outside of the shape layer, so that pins can take priority over the shape + const outOfLayerClickedFeatures = [] + // Get the features at the click position - const feature = map.forEachFeatureAtPixel(evt.pixel, layerFilter, { - hitTolerance: 5 - }) - if (feature) { - const matchingResult = props.results.find((itm) => { - return itm.field_postcode[0] === feature?.get('postcode') - }) + map.forEachFeatureAtPixel( + evt.pixel, + (f, layer) => { + if (layer.get('title') === layerIdentifier) { + clickedFeatures.push(f) + } else { + outOfLayerClickedFeatures.push(f) + } + }, + { + hitTolerance: 5 + } + ) - popup.value.isArea = true - popup.value.feature = [matchingResult] - popup.value.isOpen = true - popup.value.position = feature.getGeometry().flatCoordinates - centerOnPopup(map, popup) + if (outOfLayerClickedFeatures.length || !clickedFeatures.length) { + return } + const feature = clickedFeatures[0] + + const matchingResult = areas.value.filter((itm) => { + return itm.field_postcode[0] === feature?.get('postcode') + }) + + popup.value.isArea = true + popup.value.feature = Array.isArray(matchingResult) + ? matchingResult + : [matchingResult] + popup.value.isOpen = true + popup.value.position = evt.coordinate + centerOnPopup(map, popup) }) // Add a pointermove event listener to the map to detect shape hover map.on('pointermove', function (evt) { diff --git a/examples/nuxt-app/test/features/data-table/custom-collection.feature b/examples/nuxt-app/test/features/data-table/custom-collection.feature index 2bb288ad33..4524ec7ec1 100644 --- a/examples/nuxt-app/test/features/data-table/custom-collection.feature +++ b/examples/nuxt-app/test/features/data-table/custom-collection.feature @@ -8,7 +8,7 @@ Feature: Custom Collection And the search autocomplete request is stubbed with "/search-listing/suggestions/none" fixture Given I am using a "macbook-16" device - @mockserver + @mockserver @focus Scenario: Custom collection Given the "/api/tide/elasticsearch/sdp_data_pipelines_scl/_search" network request is stubbed with fixture "/landingpage/custom-collection/response" and status 200 as alias "cslReq" Given I visit the page "/custom-collection" diff --git a/examples/nuxt-app/test/fixtures/map-table/ise/aggregations b/examples/nuxt-app/test/fixtures/map-table/ise/aggregations new file mode 100644 index 0000000000..5a937cb750 --- /dev/null +++ b/examples/nuxt-app/test/fixtures/map-table/ise/aggregations @@ -0,0 +1,50 @@ +{ + "took": 1, + "timed_out": false, + "_shards": { + "total": 5, + "successful": 5, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 2129, + "relation": "eq" + }, + "max_score": null, + "hits": [] + }, + "aggregations": { + "category": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [ + { + "key": "Early childhood", + "doc_count": 274 + }, + { + "key": "New school", + "doc_count": 87 + }, + { + "key": "Non-government grant", + "doc_count": 198 + }, + { + "key": "Planning", + "doc_count": 13 + }, + { + "key": "School upgrade", + "doc_count": 991 + }, + { + "key": "Tech school", + "doc_count": 10 + } + ] + } + } +} diff --git a/packages/ripple-tide-search/components/global/TideSearchListingResultsMap.vue b/packages/ripple-tide-search/components/global/TideSearchListingResultsMap.vue index a16102b93b..9d3eefc21b 100644 --- a/packages/ripple-tide-search/components/global/TideSearchListingResultsMap.vue +++ b/packages/ripple-tide-search/components/global/TideSearchListingResultsMap.vue @@ -31,11 +31,25 @@ { style.setImage( new Icon({ src: markerIconDefaultSrc, - color: icon + color: icon, + anchor: [0.5, 1], + anchorXUnits: 'fraction', + anchorYUnits: 'fraction' }) ) } else { diff --git a/packages/ripple-ui-maps/src/components/map/RplMap.vue b/packages/ripple-ui-maps/src/components/map/RplMap.vue index 1116a34769..50ab9946ed 100644 --- a/packages/ripple-ui-maps/src/components/map/RplMap.vue +++ b/packages/ripple-ui-maps/src/components/map/RplMap.vue @@ -42,7 +42,10 @@ const props = withDefaults(defineProps(), { let color = feature.color || 'red' const ic = new Icon({ src: markerIconDefaultSrc, - color + color, + anchor: [0.5, 1], + anchorXUnits: 'fraction', + anchorYUnits: 'fraction' }) ic.load() return ic @@ -105,6 +108,7 @@ function onMapSingleClick(evt) { if (point.features.length > 1) { // if we click on a cluster we zoom to fit all the items in view zoomToClusterExtent(point.features, popup, map, props.projection) + popup.value.isOpen = false } else if (point.features.length === 1) { // if we click on a pin we open the popup const clickedFeature = point.features[0] @@ -121,6 +125,10 @@ function onMapSingleClick(evt) { centerMap(map, coordinates) } } + + if (!point || !point.features || point.features.length === 0) { + popup.value.isOpen = false + } } function onMapMove(evt) { @@ -200,7 +208,11 @@ function onMapMove(evt) { - + (), { areaDataKey: 'postcode' }) -const mappedAreas = computed(() => { - const matches = props.results - .filter((itm) => !itm.lat && itm.get(props.areaDataKey) !== undefined) - .map((area) => `'${area.get(props.areaDataKey)}'`) +const areas = computed(() => { + const matches = props.results.filter((itm) => { + const itemProperties = itm.getProperties() + return ( + !itemProperties.lat && itemProperties[props.areaDataKey] !== undefined + ) + }) return matches }) + +const mappedAreas = computed(() => { + return areas.value.map((area) => `'${area.get(props.areaDataKey)}'`) +}) const shapeFormat = new GeoJSON() const areaUrl = computed(() => { @@ -99,21 +106,41 @@ onMounted(async () => { } map.on('singleclick', function (evt) { + const clickedFeatures = [] + // We need to keep track of features that are clicked outside of the shape layer, so that pins can take priority over the shape + const outOfLayerClickedFeatures = [] + // Get the features at the click position - const feature = map.forEachFeatureAtPixel(evt.pixel, layerFilter, { - hitTolerance: 5 - }) - if (feature) { - const matchingResult = props.results.find((itm) => { - return itm.postcode === feature?.get('postcode') - }) + map.forEachFeatureAtPixel( + evt.pixel, + (f, layer) => { + if (layer.get('title') === layerIdentifier) { + clickedFeatures.push(f) + } else { + outOfLayerClickedFeatures.push(f) + } + }, + { + hitTolerance: 5 + } + ) - popup.value.isArea = true - popup.value.feature = [matchingResult] - popup.value.isOpen = true - popup.value.position = feature.getGeometry().flatCoordinates - centerOnPopup(map, popup) + if (outOfLayerClickedFeatures.length || !clickedFeatures.length) { + return } + const feature = clickedFeatures[0] + + const matchingResult = areas.value.find((itm) => { + return ( + itm.getProperties().postcode === feature?.getProperties().postcode + ) + }) + + popup.value.isArea = true + popup.value.feature = [matchingResult.getProperties()] + popup.value.isOpen = true + popup.value.position = feature.getGeometry().flatCoordinates + centerOnPopup(map, popup) }) // Add a pointermove event listener to the map to detect shape hover map.on('pointermove', function (evt) { diff --git a/packages/ripple-ui-maps/src/components/popup/RplMapPopUp.css b/packages/ripple-ui-maps/src/components/popup/RplMapPopUp.css index 38f3c7c7e6..bbf5650c34 100644 --- a/packages/ripple-ui-maps/src/components/popup/RplMapPopUp.css +++ b/packages/ripple-ui-maps/src/components/popup/RplMapPopUp.css @@ -3,31 +3,45 @@ .rpl-map-popup { position: absolute; height: calc(600px - var(--rpl-sp-4) * 2); - background-color: #fff; - z-index: 99; - margin: var(--rpl-sp-4); + background-color: var(--rpl-clr-light); + z-index: var(--rpl-z-index-4); box-shadow: var(--rpl-elevation-1); border-radius: var(--rpl-border-radius-2); + border: var(--rpl-border-1) solid var(--rpl-clr-neutral-300); + margin: 0 auto; + margin-top: var(--rpl-sp-1); + @media (--rpl-bp-s) { width: 300px; } + + @media (--rpl-bp-m) { + margin-top: var(--rpl-sp-2); + } } + .rpl-map-popup__large-pin { transition: transform 0.15s ease-in; position: absolute; left: 50%; - top: -30px; - margin-left: -38px; + top: -20px; + margin-left: -14px; height: 33px; - width: 75px; + width: 28px; + svg { width: 100%; height: 100%; } + + @media (--rpl-bp-m) { + top: -22px; + } } .rpl-map-popup__large-pin--open { - transform: scale(2) translateY(-8px); + transform-origin: center 50%; + transform: scale(2) translateY(-14px); } .rpl-map-popup-enter-active, @@ -43,6 +57,8 @@ .rpl-map-popup__header { display: flex; flex-direction: row; + align-items: baseline; + column-gap: var(--rpl-sp-6); color: var(--rpl-clr-dark); background-color: var(--rpl-clr-neutral-200); padding: var(--rpl-sp-4); @@ -50,39 +66,33 @@ border-radius: var(--rpl-border-radius-2) var(--rpl-border-radius-2) 0 0; } -.rpl-map-popup__body { - padding: var(--rpl-sp-4); -} - .rpl-map-popup__close { margin-left: auto; border-radius: var(--rpl-border-radius-3); height: var(--rpl-sp-6); width: var(--rpl-sp-6); + .rpl-icon { color: var(--rpl-clr-primary); } - &:hover { + + &:focus { .rpl-icon { color: var(--rpl-clr-primary-contrast); } } } - - .rpl-map-popup--popover { position: relative; width: calc(100% - (var(--rpl-sp-4) * 2)); height: auto; display: block; - @media (--rpl-bp-s) { - width: 250px; - } + max-width: 300px; } .rpl-map-popup--popover.rpl-map-popup--area { - &:before { + &::before { position: absolute; content: ''; left: 50%; @@ -90,12 +100,60 @@ width: 12px; height: 12px; background-color: var(--rpl-clr-neutral-200); + border-top: var(--rpl-border-1) solid var(--rpl-clr-neutral-300); + border-right: var(--rpl-border-1) solid var(--rpl-clr-neutral-300); transform: rotate(-45deg); } } .rpl-map-popup--popover .rpl-map-popup__body { max-height: 200px; - overflow: scroll; } +.rpl-map-pop-up-accordion-item { + position: relative; + border-bottom: solid var(--rpl-border-1) var(--rpl-clr-neutral-300); +} + +.rpl-map-pop-up-accordion-item__toggle { + display: flex; + align-items: flex-start; + justify-content: space-between; + width: 100%; + padding-left: var(--rpl-sp-4); + padding-right: var(--rpl-sp-4); + padding-top: var(--rpl-sp-3); + padding-bottom: calc(var(--rpl-sp-3) - var(--rpl-border-1)); + background: none; + border: 0; + text-align: left; + cursor: pointer; +} + +.rpl-map-pop-up-accordion-item__chevron { + flex-shrink: 0; + display: flex; + margin-top: var(--rpl-sp-1); + margin-left: var(--rpl-sp-2); + color: var(--rpl-clr-link); + transition: transform var(--rpl-motion-speed-7) linear; + + @media (--rpl-bp-l) { + margin-top: var(--rpl-sp-2); + margin-left: var(--rpl-sp-6); + } + + .rpl-map-pop-up-accordion-item--active & { + transform: rotate(-180deg); + } +} + +.rpl-map-pop-up-scroll-container { + overflow-y: auto; + background: linear-gradient(white 33%, rgb(255 255 255 / 0%)), + linear-gradient(to bottom, rgb(25 25 25 / 8%), transparent); + background-color: white; + background-repeat: no-repeat; + background-attachment: local, scroll; + background-size: 100% 45px, 100% 1.6rem; +} diff --git a/packages/ripple-ui-maps/src/components/popup/RplMapPopUp.stories.mdx b/packages/ripple-ui-maps/src/components/popup/RplMapPopUp.stories.mdx new file mode 100644 index 0000000000..cd591bbc82 --- /dev/null +++ b/packages/ripple-ui-maps/src/components/popup/RplMapPopUp.stories.mdx @@ -0,0 +1,49 @@ +import { + Canvas, + Meta, + Story, + ArgsTable +} from '@storybook/addon-docs' +import RplMapPopUp from './RplMapPopUp.vue' +import '@dpc-sdp/ripple-ui-core/style/components' + +export const SingleTemplate = (args) => ({ + components: { RplMapPopUp }, + setup() { + return { args } + }, + template: ` + + ` +}) + + + +# Tag + + + +## Default + + + + {SingleTemplate.bind()} + + diff --git a/packages/ripple-ui-maps/src/components/popup/RplMapPopUp.vue b/packages/ripple-ui-maps/src/components/popup/RplMapPopUp.vue index fbff0251e9..c6e93dbf98 100644 --- a/packages/ripple-ui-maps/src/components/popup/RplMapPopUp.vue +++ b/packages/ripple-ui-maps/src/components/popup/RplMapPopUp.vue @@ -1,8 +1,9 @@