diff --git a/app/assets/stylesheets/map.scss b/app/assets/stylesheets/map.scss new file mode 100644 index 000000000..6fd80c125 --- /dev/null +++ b/app/assets/stylesheets/map.scss @@ -0,0 +1,173 @@ +@import 'leaflet/dist/leaflet'; + +// Styles to make the leaflet map match GOVUK (accessibility-focused) styling + +@import "settings/all"; +@import "helpers/all"; + +// Reset focus colour to latest version, to match the rest of the app +$govuk-focus-colour: #FFDD00; +$zoom-button-colour: govuk-colour("white"); +$zoom-button-hover-colour: govuk-shade($zoom-button-colour, 10%); + +// The map focus style is a 2-colour outline, made to match the GOVUK text input focus style, using +// box-shadow. When colours are overridden, for example when users have Windows high contrast mode +// on, box-shadows disappear, so the outline is left as a single colour (defined by the OS, to +// replace 'transparent') which we offset from the map to mimic the 2-colour version. +// +// Leaflet adds focus styles with JS, through inline styles. Because of their higher precedence +// we need to mark our overrides with !important. +// +// This approach, taken from MDN, allows it to fall back to :focus for browsers without support: +// https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible#selectively_showing_the_focus_indicator +.leaflet-container { + + &:focus { + box-shadow: 0px 0px 0 3px $govuk-focus-text-colour, 0 0 0 6px $govuk-focus-colour; + outline: solid 3px transparent !important; // sass-lint:disable-line no-important + outline-offset: 3px; + } + + // The map should only recieve focus from being tabbed to. This stops the focus state coming + // from other sources (like clicks on child elements). + &:focus:not(:focus-visible) { + box-shadow: none; + outline: none !important; // sass-lint:disable-line no-important + } +} + +// Overrides for zoom controls to make them match GOVUK buttons +// https://design-system.service.gov.uk/components/button/ +// +// GOVUK buttons usually have an outline that shows up in high contrast modes. Our buttons are cropped by +// their container so we use their border instead. +// +// Also introduces a thick black divider between the buttons, made with a pseudo element, appearing +// on focus. Together with the yellow background, this copies the 2-colour style GOVUK Frontend +// buttons and links have. +.leaflet-bar { + border-radius: 0; // Remove rounded corners + background: #CCCCCC; // Grey background to give a thin divider between buttons when they're not in focus + + @media (forced-colors: active) { + background: canvasText; + } + + a { + + // Outlines don't work because of the buttons being so close together so turn off + outline: none; + + &:first-child { + border-bottom: none; + margin-bottom: 1px; // Thin divider to distinguish buttons + } + + &:last-child { + // Allow it to contain the absolutely positioned divider we show between buttons + // when one is focused + position: relative; + } + + // Hover style is darker background + &:hover { + background-color: $zoom-button-hover-colour; + } + + // Styles that apply if focused with or without the :hover or :active state + &:focus { + // Button focus is shown by a change in background colour. + // When colours are overridden, for example when users have Windows high + // contrast mode on, backgrounds disappear, so we need to ensure there's a + // visible outline so it can be set to a colour by the OS. + border: solid 2px $govuk-focus-colour; + box-sizing: border-box; // make sure height includes the border + line-height: 22px; // subtract the border from the height (normally 26px) + + // The inline display box for the button text overlaps the button edge. + // This is only visible in high contrast mode because of the background colour being set. + @media (forced-colors: active) { + overflow: hidden; + } + } + + // Focused style sets the button to the focus colour + &:focus:not(:active):not(:hover) { + color: $govuk-focus-text-colour; + background-color: $govuk-focus-colour; + } + + // Shared styles for the divider between buttons that appears when either are focused + &:last-child:focus:not(:active):not(:hover):before, + &:first-child:focus:not(:active):not(:hover) + a:before { + content: ""; + position: absolute; + height: 2px; + background: $govuk-focus-text-colour; + + // Hide in forced-color mode, since the button outlines suffice + @media (forced-colors: active) { + display: none; + } + } + + // Styles for the divider, specifc to the second button being focused + &:last-child:focus:not(:active):not(:hover):before { + left: -2px; // Subtract 2px left border + top: -3px; // Shift divider up 1px so it overlaps buttons equally, and another 2px to compensate for the reduced height of the last button (due to the border) + width: calc(100% + 4px); // Compensate for borders + } + + // Styles for the divider, specifc to the first button being focused + &:first-child:focus:not(:active):not(:hover) + a:before { + left: 0px; // No borders on parent of :before when not focused + top: -1px; // Shift divider up 1px so it overlaps buttons equally + width: 100%; + } + } + +} + +// Extra block to override specific LeafletJS rounded-corners when buttons are focused +.leaflet-bar a, +.leaflet-touch .leaflet-bar a { + + &:first-child { + // Remove rounded corners + border-top-left-radius: 0; + border-top-right-radius: 0; + } + + &:last-child { + // Remove rounded corners + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + +} + +// buttons are 30px for touch-capable devices/browsers +.leaflet-touch .leaflet-bar a:focus { + line-height: 26px; // subtract the border from the height (normally 30px) +} + +// Map attribution links +.leaflet-control-attribution { + & a { + &:focus { + // Link focus is shown by a change in background colour. + // When colours are overridden, for example when users have Windows high + // contrast mode on, backgrounds disappear, so we need to ensure there's a + // visible outline so it can be set to a colour by the OS. + outline: $govuk-focus-width solid transparent !important; // sass-lint:disable-line no-important + color: $govuk-focus-text-colour; + background: $govuk-focus-colour; + box-shadow: 0 -2px $govuk-focus-colour, 0 4px $govuk-focus-text-colour; + } + + // use an extra prefix class to override LeafletJS CSS + .leaflet-bottom &:hover { + text-decoration: none; + } + } +} diff --git a/app/templates/views/broadcast/partials/area-map-javascripts.html b/app/templates/views/broadcast/partials/area-map-javascripts.html index 5a401b8fd..fdb7af5f7 100644 --- a/app/templates/views/broadcast/partials/area-map-javascripts.html +++ b/app/templates/views/broadcast/partials/area-map-javascripts.html @@ -29,16 +29,82 @@ var mapElement = document.getElementById('area-list-map'); + var grandparent = mapElement.parentNode.parentNode; + var isInDetails = grandparent.className.indexOf('govuk-details') !== -1; + var areas = document.querySelectorAll('.area-list > .area-list-item'); + var trimRegExp = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; + var details; + var label; // if element is inside a details element then to make the map render correctly we open the details element // and set up the map before closing the details map after - grandparent = mapElement.parentElement.parentElement; - if (grandparent.className.split(/\s+/).indexOf('govuk-details') > -1) { + if (isInDetails) { details = grandparent; details.open = true; } + function addAriaLabel (mapElement) { + + function getLabelPrefix (areas) { + return [ + 'Map of the United Kingdom, showing the ', + (areas.length === 1) ? 'area' : 'areas', + ' for' + ].join(''); + }; + + function getStringOfAreas (areas) { + var areasLen = areas.length; + var areaStrings = []; + var idx; + + function getAreaName (area) { + var areaString = ''; + var childNodesLen = area.childNodes.length; + var idx; + var childNode; + + for (idx = 0; idx < childNodesLen; idx++) { + childNode = area.childNodes[idx]; + if (childNode.nodeType === 3) { areaString += childNode.nodeValue; } // only use text nodes + } + + return areaString.replace(trimRegExp, '') + }; + + if (areasLen === 1) { return getAreaName(areas[0]); } + + for (idx = 0; idx < areasLen; idx++) { + areaStrings.push(getAreaName(areas[idx])); + } + + // always join last 2 areas with 'and', the rest with commas + return areaStrings.slice(0, areasLen - 1).join(', ') + ' and ' + areaStrings[areasLen - 1]; + }; + + label = getLabelPrefix(areas) + " " + getStringOfAreas(areas); + mapElement.setAttribute('aria-label', label); + }; + + function addAriaDescription (mapElement) { + + var mapContainer = mapElement.parentNode; + var description = document.createElement('p'); + var descriptionText = 'Use the arrow keys to move the map. Use the buttons to zoom the map in or out'; + + description.appendChild( + document.createTextNode(descriptionText) + ); + description.setAttribute('id', 'area-list-map__description'); + description.className = 'govuk-visually-hidden'; + mapContainer.insertBefore(description, mapElement.nextSibling); + mapElement.setAttribute('aria-describedby', 'area-list-map__description'); + }; + mapElement.style.height = Math.max(320, window.innerHeight - mapElement.offsetTop - 100) + 'px'; + mapElement.setAttribute('role', 'region'); + addAriaLabel(mapElement); + addAriaDescription(mapElement); var mymap = L.map( 'area-list-map', @@ -58,7 +124,7 @@ ); // if element is inside a details element then close the details element - if (details) { + if (isInDetails) { details.open = false; } diff --git a/app/templates/views/broadcast/partials/area-map-stylesheets.html b/app/templates/views/broadcast/partials/area-map-stylesheets.html index e007724a1..84e8c32a6 100644 --- a/app/templates/views/broadcast/partials/area-map-stylesheets.html +++ b/app/templates/views/broadcast/partials/area-map-stylesheets.html @@ -1 +1 @@ - + diff --git a/gulpfile.js b/gulpfile.js index 9dbd998ee..4412a1ddd 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -202,8 +202,8 @@ const javascripts = () => { const sass = () => { return src([ paths.src + '/stylesheets/main*.scss', - paths.src + '/stylesheets/print.scss', - paths.npm + '/leaflet/dist/leaflet.css' + paths.src + '/stylesheets/map.scss', + paths.src + '/stylesheets/print.scss' ]) .pipe(plugins.prettyerror()) .pipe(plugins.sass({ @@ -212,6 +212,7 @@ const sass = () => { paths.npm + 'govuk-elements-sass/public/sass/', paths.toolkit + 'stylesheets/', paths.govuk_frontend, + paths.npm ] })) .pipe(plugins.cssUrlAdjuster({