-
Notifications
You must be signed in to change notification settings - Fork 339
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a5281d4
commit b77bf86
Showing
9 changed files
with
577 additions
and
0 deletions.
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
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
Empty file.
164 changes: 164 additions & 0 deletions
164
src/govuk/components/modal-dialogue/_modal-dialogue.scss
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,164 @@ | ||
// Disables linting for this file only | ||
// sass-lint:disable no-duplicate-properties | ||
|
||
@import "../../settings/all"; | ||
@import "../../tools/all"; | ||
@import "../../helpers/all"; | ||
|
||
@include govuk-exports("govuk/component/modal-dialogue") { | ||
$govuk-dialogue-width: 640px; | ||
|
||
.govuk-modal-dialogue, | ||
.govuk-modal-dialogue__backdrop { | ||
position: fixed; | ||
z-index: 0; | ||
top: 0; | ||
left: 0; | ||
width: 100%; | ||
height: 100%; | ||
} | ||
|
||
// Hide dialogue when closed | ||
.govuk-modal-dialogue { | ||
display: none; | ||
} | ||
|
||
// Show dialogue when opened | ||
.govuk-modal-dialogue--open { | ||
display: block; | ||
} | ||
|
||
// Wrapper to handle overflow scrolling | ||
.govuk-modal-dialogue__wrapper { | ||
box-sizing: border-box; | ||
display: flex; | ||
height: 100%; | ||
@include govuk-responsive-padding(7, "top"); | ||
@include govuk-responsive-padding(7, "bottom"); | ||
overflow-y: auto; | ||
align-items: flex-start; | ||
align-items: safe center; | ||
} | ||
|
||
// HTML5 dialogue component | ||
.govuk-modal-dialogue__box { | ||
box-sizing: border-box; | ||
display: block; | ||
position: relative; | ||
z-index: 1; | ||
width: 90%; | ||
margin: auto; | ||
padding: 0; | ||
overflow-y: auto; | ||
border: $govuk-focus-width solid govuk-colour("black"); | ||
background: govuk-colour("white"); | ||
|
||
// Add focus outline to dialogue | ||
&:focus { | ||
outline: $govuk-focus-width solid $govuk-focus-colour; | ||
} | ||
|
||
// Hide browser backdrop | ||
&::backdrop { | ||
display: none; | ||
} | ||
} | ||
|
||
// Header with close button | ||
.govuk-modal-dialogue__header { | ||
@include govuk-clearfix; | ||
@include govuk-responsive-margin(5, "bottom"); | ||
padding-bottom: $govuk-focus-width; | ||
color: govuk-colour("white"); | ||
background-color: govuk-colour("black"); | ||
text-align: right; | ||
} | ||
|
||
// Inner content | ||
.govuk-modal-dialogue__content { | ||
@include govuk-font($size: 16); | ||
@include govuk-responsive-padding(6); | ||
padding-top: 0; | ||
} | ||
|
||
.govuk-modal-dialogue__description { | ||
@include govuk-responsive-margin(4, "bottom"); | ||
} | ||
|
||
// Remove bottom margins | ||
.govuk-modal-dialogue__description:last-child, | ||
.govuk-modal-dialogue__description > :last-child, | ||
.govuk-modal-dialogue__content > :last-child { | ||
margin-bottom: 0; | ||
} | ||
|
||
// Custom backdrop | ||
.govuk-modal-dialogue__backdrop { | ||
opacity: .8; | ||
background: govuk-colour("black"); | ||
pointer-events: none; | ||
touch-action: none; | ||
} | ||
|
||
// Crown icon | ||
.govuk-modal-dialogue__crown { | ||
display: block; | ||
margin: 6px 0 0 6px; | ||
@include govuk-responsive-margin(5, "left"); | ||
float: left; | ||
} | ||
|
||
// Heading | ||
.govuk-modal-dialogue__heading:last-child { | ||
margin-bottom: 0; | ||
} | ||
|
||
// Close button | ||
.govuk-modal-dialogue__close { | ||
$font-size: 36px; | ||
$line-height: 1; | ||
|
||
display: block; | ||
width: auto; | ||
min-width: 44px; | ||
margin: 0; | ||
padding: 2px 5px; | ||
float: right; | ||
color: govuk-colour("white"); | ||
background-color: govuk-colour("black"); | ||
box-shadow: none !important; | ||
font-size: $font-size; | ||
@if $govuk-typography-use-rem { | ||
font-size: govuk-px-to-rem($font-size); | ||
} | ||
@include govuk-typography-weight-bold; | ||
line-height: $line-height; | ||
|
||
&:hover { | ||
color: govuk-colour("black"); | ||
background-color: govuk-colour("yellow"); | ||
} | ||
|
||
&:active { | ||
top: 0; | ||
} | ||
} | ||
|
||
// New dialogue width, inline button + link | ||
@include govuk-media-query($from: tablet) { | ||
.govuk-modal-dialogue__content { | ||
padding-top: 0; | ||
} | ||
|
||
.govuk-modal-dialogue__box { | ||
width: percentage($govuk-dialogue-width / map-get($govuk-breakpoints, desktop)); | ||
} | ||
} | ||
|
||
// Fixed width | ||
@include govuk-media-query($from: desktop) { | ||
.govuk-modal-dialogue__box { | ||
width: $govuk-dialogue-width; | ||
} | ||
} | ||
} |
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,3 @@ | ||
{% macro govukModalDialogue(params) %} | ||
{%- include "./template.njk" -%} | ||
{% endmacro %} |
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,188 @@ | ||
import { nodeListForEach } from '../../common' | ||
import '../../vendor/polyfills/Element/prototype/classList' | ||
import '../../vendor/polyfills/Function/prototype/bind' | ||
import '../../vendor/polyfills/Event' // addEventListener and event.target normaliziation | ||
|
||
function ModalDialogue ($module) { | ||
this.$module = $module | ||
this.$dialogBox = $module.querySelector('dialog') | ||
this.$container = document.documentElement | ||
|
||
// Check for browser support | ||
this.hasNativeDialog = 'showModal' in this.$dialogBox | ||
|
||
// Allowed focussable elements | ||
this.focussable = [ | ||
'button', | ||
'[href]', | ||
'input', | ||
'select', | ||
'textarea' | ||
] | ||
} | ||
|
||
// Initialize component | ||
ModalDialogue.prototype.init = function (options) { | ||
this.options = options || {} | ||
|
||
this.open = this.handleOpen.bind(this) | ||
this.close = this.handleClose.bind(this) | ||
this.focus = this.handleFocus.bind(this) | ||
this.boundKeyDown = this.handleKeyDown.bind(this) | ||
|
||
// Elements to allow focus on | ||
this.$focussable = this.$dialogBox.querySelectorAll(this.focussable.toString()) | ||
this.$focusableLast = this.$focussable[this.$focussable.length - 1] | ||
this.$focusElement = this.options.focusElement || this.$dialogBox | ||
|
||
// Close button | ||
this.$buttonClose = this.$dialogBox.querySelector('.govuk-modal-dialogue__close') | ||
|
||
// Default open state | ||
this.isOpen = this.$dialogBox.hasAttribute('open') | ||
|
||
if (this.isOpen) { | ||
this.open() | ||
} | ||
|
||
// Optional trigger element | ||
if (this.options.triggerElement) { | ||
this.options.triggerElement.addEventListener('click', this.open) | ||
} | ||
|
||
// Close dialogue on close button click | ||
this.$buttonClose.addEventListener('click', this.close) | ||
|
||
return this | ||
} | ||
|
||
// Open modal | ||
ModalDialogue.prototype.handleOpen = function (event) { | ||
if (event) { | ||
event.preventDefault() | ||
} | ||
|
||
// Save last-focussed element | ||
this.$lastActiveElement = document.activeElement | ||
|
||
// Disable scrolling | ||
this.$container.classList.add('govuk-!-scroll-disabled') | ||
|
||
// Close on escape key, trap focus | ||
document.addEventListener('keydown', this.boundKeyDown, true) | ||
|
||
// Optional 'onOpen' callback | ||
if (typeof this.options.onOpen === 'function') { | ||
this.options.onOpen.call(this) | ||
} | ||
|
||
// Skip open if already open | ||
if (this.isOpen) { | ||
return | ||
} | ||
|
||
// Show wrapper | ||
this.$module.classList.add('govuk-modal-dialogue--open') | ||
|
||
// Show modal | ||
this.hasNativeDialog | ||
? this.$dialogBox.show() | ||
: this.$dialogBox.setAttribute('open', '') | ||
|
||
// Mark open, handle focus | ||
this.isOpen = true | ||
this.focus() | ||
} | ||
|
||
// Close modal | ||
ModalDialogue.prototype.handleClose = function (event) { | ||
if (event) { | ||
event.preventDefault() | ||
} | ||
|
||
// Skip close if already closed | ||
if (!this.isOpen) { | ||
return | ||
} | ||
|
||
// Hide modal | ||
this.hasNativeDialog | ||
? this.$dialogBox.close() | ||
: this.$dialogBox.removeAttribute('open') | ||
|
||
// Hide wrapper, enable scrolling | ||
this.$module.classList.remove('govuk-modal-dialogue--open') | ||
this.$container.classList.remove('govuk-!-scroll-disabled') | ||
|
||
// Mark as closed | ||
this.isOpen = false | ||
|
||
// Restore focus to last active element | ||
this.$lastActiveElement.focus() | ||
|
||
// Optional 'onClose' callback | ||
if (typeof this.options.onClose === 'function') { | ||
this.options.onClose.call(this) | ||
} | ||
|
||
// Remove escape key and trap focus listener | ||
document.removeEventListener('keydown', this.boundKeyDown, true) | ||
} | ||
|
||
// Lock scroll, focus modal | ||
ModalDialogue.prototype.handleFocus = function () { | ||
this.$dialogBox.scrollIntoView() | ||
this.$focusElement.focus({ preventScroll: true }) | ||
} | ||
|
||
// Listen for key presses | ||
ModalDialogue.prototype.handleKeyDown = function (event) { | ||
var KEY_TAB = 9 | ||
var KEY_ESCAPE = 27 | ||
|
||
switch (event.keyCode) { | ||
case KEY_TAB: | ||
var $focusElement | ||
|
||
// Check for tabbing outside dialog | ||
var hasFocusEscaped = document.activeElement !== this.$dialogBox | ||
|
||
// Loop inner focussable elements | ||
if (hasFocusEscaped) { | ||
nodeListForEach(this.$focussable, function (element) { | ||
// Actually, focus is on an inner focussable element | ||
if (hasFocusEscaped && document.activeElement === element) { | ||
hasFocusEscaped = false | ||
} | ||
}) | ||
|
||
// Wrap focus back to first element | ||
$focusElement = hasFocusEscaped | ||
? this.$dialogBox | ||
: undefined | ||
} | ||
|
||
// Wrap focus back to first/last element | ||
if (!$focusElement) { | ||
if ((document.activeElement === this.$focusableLast && !event.shiftKey) || !this.$focussable.length) { | ||
$focusElement = this.$dialogBox | ||
} else if (document.activeElement === this.$dialogBox && event.shiftKey) { | ||
$focusElement = this.$focusableLast | ||
} | ||
} | ||
|
||
// Wrap focus | ||
if ($focusElement) { | ||
event.preventDefault() | ||
$focusElement.focus({ preventScroll: true }) | ||
} | ||
|
||
break | ||
|
||
case KEY_ESCAPE: | ||
this.close() | ||
break | ||
} | ||
} | ||
|
||
export default ModalDialogue |
Oops, something went wrong.