-
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
e346314
commit aba92ba
Showing
9 changed files
with
572 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.
161 changes: 161 additions & 0 deletions
161
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,161 @@ | ||
@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; // sass-lint:disable no-duplicate-properties | ||
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,186 @@ | ||
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.focusTrap = this.handleFocusTrap.bind(this) | ||
this.boundKeyDown = this.handleKeyDown.bind(this) | ||
|
||
// Modal elements | ||
this.$buttonClose = this.$dialogBox.querySelector('.govuk-modal-dialogue__close') | ||
this.$focussable = this.$dialogBox.querySelectorAll(this.focussable.toString()) | ||
this.$focusableLast = this.$focussable[this.$focussable.length - 1] | ||
this.$focusElement = this.options.focusElement || this.$dialogBox | ||
|
||
if (this.$dialogBox.hasAttribute('open')) { | ||
this.open() | ||
} | ||
|
||
this.initEvents() | ||
|
||
return this | ||
} | ||
|
||
// Initialize component events | ||
ModalDialogue.prototype.initEvents = function (options) { | ||
if (this.options.triggerElement) { | ||
this.options.triggerElement.addEventListener('click', this.open) | ||
} | ||
|
||
// Close dialogue on close button click | ||
this.$buttonClose.addEventListener('click', this.close) | ||
} | ||
|
||
// Open modal | ||
ModalDialogue.prototype.handleOpen = function (event) { | ||
if (event) { | ||
event.preventDefault() | ||
} | ||
|
||
// Save last-focussed element | ||
this.$lastActiveElement = document.activeElement | ||
|
||
// Disable scrolling, show wrapper | ||
this.$container.classList.add('govuk-!-scroll-disabled') | ||
this.$module.classList.add('govuk-modal-dialogue--open') | ||
|
||
// 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.$dialogBox.hasAttribute('open')) { | ||
return | ||
} | ||
|
||
// Show modal | ||
this.hasNativeDialog | ||
? this.$dialogBox.show() | ||
: this.$dialogBox.setAttribute('open', '') | ||
|
||
// Handle focus | ||
this.focus() | ||
} | ||
|
||
// Close modal | ||
ModalDialogue.prototype.handleClose = function (event) { | ||
if (event) { | ||
event.preventDefault() | ||
} | ||
|
||
// Skip close if already closed | ||
if (!this.$dialogBox.hasAttribute('open')) { | ||
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') | ||
|
||
// 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 }) | ||
} | ||
|
||
// Ensure focus stays within modal | ||
ModalDialogue.prototype.handleFocusTrap = function (event) { | ||
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 }) | ||
} | ||
} | ||
|
||
// Listen for key presses | ||
ModalDialogue.prototype.handleKeyDown = function (event) { | ||
var KEY_TAB = 9 | ||
var KEY_ESCAPE = 27 | ||
|
||
switch (event.keyCode) { | ||
case KEY_TAB: | ||
this.focusTrap(event) | ||
break | ||
|
||
case KEY_ESCAPE: | ||
this.close() | ||
break | ||
} | ||
} | ||
|
||
export default ModalDialogue |
Oops, something went wrong.