Skip to content

Commit f35b24f

Browse files
authored
Merge pull request boostorg#2183 from boostorg/2156-dialog-component
Adds dialog component and template variable debugging
2 parents c5a344c + 6afaf7f commit f35b24f

9 files changed

Lines changed: 348 additions & 12 deletions

File tree

flake.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
python313.pkgs.black
6868
python313.pkgs.isort
6969
python313.pkgs.pip-tools
70+
git
7071
];
7172
# Host system installation workflow goes into the bootstrap justfile target.
7273
# Project specific installation and execution workflow should go here.

static/css/v3/components.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@
3030
@import "./banner.css";
3131
@import "./animations.css";
3232
@import "./account-connections.css";
33+
@import "./dialog.css";

static/css/v3/dialog.css

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/**
2+
* Dialog Component
3+
*
4+
* A modal dialog triggered by user action, requiring an interstitial step.
5+
* Dialog Container (.dialog-modal__container) is the inner content area and
6+
* is NOT intended as a standalone module – it only exists inside Dialog Modal.
7+
*
8+
* Open/close is CSS-only via the :target pseudo-class. No JavaScript required.
9+
* Trigger: <a href="#dialog-id">…</a>
10+
* Close: <a href="#">…</a> (close button and backdrop link inside the dialog)
11+
*
12+
* Structure:
13+
* .dialog-modal – full-screen overlay (hidden by default)
14+
* .dialog-modal__backdrop – invisible close link covering the overlay area
15+
* .dialog-modal__container – centred content card
16+
* .dialog-modal__header – title row + close link
17+
* .dialog-modal__title – heading text
18+
* .dialog-modal__close – dismiss link (×)
19+
* .dialog-modal__description – optional body text
20+
* .dialog-modal__buttons – primary / secondary action row
21+
*
22+
* Light/dark: all colour tokens are already theme-aware via semantics.css + themes.css.
23+
*/
24+
25+
/* ============================================
26+
DIALOG MODAL (full-screen overlay)
27+
============================================ */
28+
.dialog-modal {
29+
display: none;
30+
position: fixed;
31+
inset: 0;
32+
z-index: 10000;
33+
background-color: var(--color-surface-modal);
34+
align-items: center;
35+
justify-content: center;
36+
}
37+
38+
.dialog-modal:target {
39+
display: flex !important;
40+
}
41+
42+
/* ============================================
43+
BACKDROP CLOSE LINK
44+
Fills the overlay; clicking outside the container closes the dialog.
45+
============================================ */
46+
.dialog-modal__backdrop {
47+
position: absolute;
48+
inset: 0;
49+
cursor: default;
50+
z-index: 0;
51+
}
52+
53+
/* ============================================
54+
DIALOG CONTAINER (inner content card)
55+
============================================ */
56+
.dialog-modal__container {
57+
position: relative;
58+
z-index: 1;
59+
display: flex;
60+
flex-direction: column;
61+
gap: var(--space-large, 16px);
62+
width: 695px;
63+
max-width: calc(100vw - 2 * var(--space-large));
64+
background-color: var(--color-surface-page);
65+
border-radius: var(--border-radius-xl);
66+
overflow: hidden;
67+
}
68+
69+
/* Mobile: full-width dialog with minimal margins */
70+
@media (max-width: 767px) {
71+
.dialog-modal__container {
72+
width: 100%;
73+
max-width: calc(100vw - 2 * var(--space-default));
74+
margin: 0 var(--space-default);
75+
border-radius: var(--border-radius-l);
76+
}
77+
}
78+
79+
/* ============================================
80+
HEADER (title + close link on the same row)
81+
============================================ */
82+
.dialog-modal__header {
83+
display: flex !important;
84+
flex-direction: row !important;
85+
align-items: flex-start;
86+
gap: var(--space-default);
87+
padding: var(--space-large);
88+
width: 100%;
89+
box-sizing: border-box;
90+
border-bottom: 1px solid var(--color-stroke-weak);
91+
}
92+
93+
.dialog-modal__title {
94+
flex: 1 0 0 !important;
95+
min-width: 0;
96+
margin: 0 !important;
97+
padding: 0 !important;
98+
font-family: var(--font-display);
99+
font-size: var(--font-size-large);
100+
font-weight: var(--font-weight-medium);
101+
line-height: var(--line-height-tight);
102+
letter-spacing: var(--letter-spacing-display-regular);
103+
color: var(--color-text-primary);
104+
display: block !important;
105+
}
106+
107+
/* ============================================
108+
CLOSE LINK (×)
109+
============================================ */
110+
.dialog-modal__close {
111+
flex-shrink: 0;
112+
display: inline-flex;
113+
align-items: center;
114+
justify-content: center;
115+
width: 24px;
116+
height: 24px;
117+
color: var(--color-icon-primary);
118+
text-decoration: none;
119+
}
120+
121+
.dialog-modal__close:hover {
122+
color: var(--color-icon-secondary);
123+
}
124+
125+
.dialog-modal__close svg {
126+
display: block;
127+
width: 24px;
128+
height: 24px;
129+
}
130+
131+
/* ============================================
132+
DESCRIPTION (optional)
133+
============================================ */
134+
.dialog-modal__description {
135+
padding: 0 var(--space-large);
136+
width: 100%;
137+
box-sizing: border-box;
138+
}
139+
140+
.dialog-modal__description p {
141+
margin: 0;
142+
font-family: var(--font-sans);
143+
font-size: var(--font-size-base);
144+
font-weight: var(--font-weight-regular);
145+
line-height: var(--line-height-default);
146+
letter-spacing: var(--letter-spacing-tight);
147+
color: var(--color-text-secondary);
148+
}
149+
150+
/* ============================================
151+
BUTTONS ROW
152+
============================================ */
153+
.dialog-modal__buttons {
154+
display: flex;
155+
flex-direction: row;
156+
gap: var(--space-default);
157+
padding: 0 var(--space-large) var(--space-large);
158+
width: 100%;
159+
box-sizing: border-box;
160+
}
161+
162+
.dialog-modal__buttons .btn {
163+
flex: 1 0 0;
164+
min-width: 128px;
165+
width: auto;
166+
text-decoration: none;
167+
}
168+
169+
.dialog-modal__buttons a.btn {
170+
text-decoration: none;
171+
}

static/css/v3/semantics.css

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,6 @@
1717
BASE SEMANTIC COLORS (Light Theme Defaults)
1818
============================================ */
1919

20-
/* Background */
21-
--color-bg-primary: var(--color-secondary-light-blue);
22-
--color-bg-secondary: var(--color-primary-white);
23-
--color-border: var(--color-primary-grey-300);
24-
2520
/* Text */
2621
--color-text-primary: var(--color-primary-black);
2722
--color-text-secondary: var(--color-primary-grey-800);
@@ -110,4 +105,17 @@
110105
--color-text-reversed: var(--color-primary-white);
111106
--color-text-reversed-on-accent: var(--color-primary-white);
112107
--color-text-tertiary: var(--color-primary-grey-700);
108+
109+
110+
111+
/* ============================================
112+
DEPRECATED
113+
114+
Please, do not use these colors. They were originally assigned as "background" colors, but this
115+
semantics doesn't exist in the Figma Foundations designs.
116+
Use "surface" colors instead.
117+
============================================ */
118+
--color-bg-primary: var(--color-secondary-light-blue);
119+
--color-bg-secondary: var(--color-primary-white);
120+
--color-border: var(--color-primary-grey-300);
113121
}

static/css/v3/themes.css

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,6 @@
1818
DARK THEME (Boost)
1919
============================================ */
2020
html.dark {
21-
/* Base Colors */
22-
--color-bg-primary: var(--color-primary-black);
23-
--color-bg-secondary: var(--color-primary-grey-950);
24-
--color-text-primary: var(--color-primary-white);
25-
--color-text-secondary: var(--color-primary-grey-400);
26-
--color-border: var(--color-primary-grey-800);
27-
2821
/* Error Primitives (Dark theme override)
2922
Note: This is a special case where a primitive token needs theme-specific opacity.
3023
In dark theme, error-weak uses reduced opacity for better contrast. */
@@ -115,4 +108,17 @@ html.dark {
115108
--color-text-on-accent: var(--color-primary-black);
116109
--color-text-reversed: var(--color-primary-black);
117110
--color-text-tertiary: var(--color-primary-grey-600);
111+
--color-text-primary: var(--color-primary-white);
112+
--color-text-secondary: var(--color-primary-grey-400);
113+
114+
/* ============================================
115+
DEPRECATED
116+
117+
Please, do not use these colors. They were originally assigned as "background" colors, but this
118+
semantics doesn't exist in the Figma Foundations designs.
119+
Use "surface" colors instead.
120+
============================================ */
121+
--color-bg-primary: var(--color-primary-black);
122+
--color-bg-secondary: var(--color-primary-grey-950);
123+
--color-border: var(--color-primary-grey-800);
118124
}

static/js/dialog.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Dialog – ESC key support and focus trap for CSS-only dialogs.
3+
*
4+
* The dialog uses CSS :target for open/close (no JS required for basic functionality).
5+
* This script adds progressive enhancement:
6+
* - ESC key closes any open dialog
7+
* - Focus trap: keeps keyboard focus within the dialog
8+
*/
9+
(function () {
10+
'use strict';
11+
12+
function getFocusableElements(container) {
13+
const focusableSelectors = [
14+
'a[href]',
15+
'button:not([disabled])',
16+
'textarea:not([disabled])',
17+
'input:not([disabled])',
18+
'select:not([disabled])',
19+
'[tabindex]:not([tabindex="-1"])'
20+
].join(', ');
21+
22+
return Array.from(container.querySelectorAll(focusableSelectors));
23+
}
24+
25+
function trapFocus(e, dialog) {
26+
if (e.key !== 'Tab') return;
27+
28+
const focusableElements = getFocusableElements(dialog);
29+
if (focusableElements.length === 0) return;
30+
31+
const firstElement = focusableElements[0];
32+
const lastElement = focusableElements[focusableElements.length - 1];
33+
const activeElement = document.activeElement;
34+
35+
const currentIndex = focusableElements.indexOf(activeElement);
36+
37+
const isForbiddenElement = currentIndex === -1 || !dialog.contains(activeElement);
38+
39+
if (e.shiftKey && (activeElement === firstElement || isForbiddenElement)) {
40+
e.preventDefault();
41+
lastElement.focus();
42+
} else if (!e.shiftKey && (activeElement === lastElement || isForbiddenElement)) {
43+
e.preventDefault();
44+
firstElement.focus();
45+
}
46+
}
47+
48+
function setInitialFocus(dialog) {
49+
const focusableElements = getFocusableElements(dialog);
50+
if (focusableElements.length > 0) {
51+
focusableElements[0].focus();
52+
}
53+
}
54+
55+
// Dialogs are opened via hash change in the URL. This listens for those specific events.
56+
window.addEventListener('hashchange', function () {
57+
const openDialogWrapper = document.querySelector('.dialog-modal:target');
58+
if (openDialogWrapper) {
59+
const dialogContainer = openDialogWrapper.querySelector('[role="dialog"]');
60+
if (dialogContainer) {
61+
setInitialFocus(dialogContainer);
62+
}
63+
}
64+
});
65+
66+
document.addEventListener('keydown', function (e) {
67+
const openDialogWrapper = document.querySelector('.dialog-modal:target');
68+
69+
if (!openDialogWrapper) return;
70+
71+
const dialogContainer = openDialogWrapper.querySelector('[role="dialog"]');
72+
if (!dialogContainer) return;
73+
74+
// ESC key closes the dialog
75+
if (e.key === 'Escape' || e.key === 'Esc') {
76+
e.preventDefault();
77+
window.location.hash = '_';
78+
return;
79+
}
80+
81+
// Tab key traps focus within the dialog
82+
trapFocus(e, dialogContainer);
83+
});
84+
})();

templates/base.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@
417417

418418
{% flag "v3" %}
419419
<script src="{% static 'js/carousel.js' %}" defer></script>
420+
<script src="{% static 'js/dialog.js' %}" defer></script>
420421
{% endflag %}
421422

422423
{% block footer_js %}{% endblock %}

templates/v3/examples/_v3_example_section.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,4 +440,15 @@ <h3 class="block-title">Code blocks</h3>
440440
{% include "v3/includes/_code_blocks_story.html" with code_standalone_1=code_demo_beast code_standalone_2=code_demo_beast code_card_1=code_demo_hello code_card_2=code_demo_beast code_card_3=code_demo_install %}
441441
</div>
442442
</div>
443+
444+
<div class="v3-examples-section__block">
445+
<h3 class="block-title">Dialog</h3>
446+
<div class="v3-examples-section__example-box">
447+
<h4 class="py-large">Dialog Modal With Description</h4>
448+
<a href="#demo-dialog-with-desc" class="btn btn-primary">Open dialog</a>
449+
</div>
450+
</div>
443451
</section>
452+
453+
{% comment %}Dialogs placed outside section to avoid position:fixed containment issues{% endcomment %}
454+
{% include "v3/includes/_dialog.html" with dialog_id="demo-dialog-with-desc" title="Title of Dialog" description="Description that can go inside of Dialog" primary_url="#_" secondary_url="#_" primary_label="Button" secondary_label="Button" %}

0 commit comments

Comments
 (0)