Skip to content

Commit 4a511ba

Browse files
authored
feat(modal): provides content-top and content-bottom slots (#6490)
**Related Issue:** #4800 ## Summary Provides `content-top` and `content-bottom` slots. For simplicity, we matched the `padding` on the `content-top` and `content-bottom` to the `content`, with one public prop for the latter. This can further be developed if requested. We decided to move forward with `content-top` and `content-bottom` naming, as this should provide enough context and also aligns with existing `<x>-start/<x>-end` naming.
1 parent 4e2f38c commit 4a511ba

File tree

4 files changed

+110
-14
lines changed

4 files changed

+110
-14
lines changed

src/components/modal/modal.scss

+38-11
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
--calcite-modal-scrim-background-internal: #{rgba($blk-240, 0.85)};
2121
}
2222

23+
.content-top[hidden],
24+
.content-bottom[hidden] {
25+
@apply hidden;
26+
}
27+
2328
.container {
2429
@apply text-color-2
2530
fixed
@@ -184,12 +189,31 @@
184189
*/
185190
.content {
186191
@apply relative box-border block h-full overflow-auto p-0;
187-
max-block-size: 100%;
188192
background-color: var(--calcite-modal-content-background, theme("colors.background.foreground.1"));
189-
padding-block: var(--calcite-modal-content-padding, var(--calcite-modal-padding-internal));
190-
padding-inline: var(--calcite-modal-content-padding, var(--calcite-modal-padding-internal));
193+
max-block-size: 100%;
194+
padding: var(--calcite-modal-content-padding, var(--calcite-modal-padding-internal));
195+
}
196+
197+
.content-top,
198+
.content-bottom {
199+
@apply bg-foreground-1 border-color-3 border-solid border-0 z-header flex;
200+
flex: 0 0 auto;
201+
padding: var(--calcite-modal-padding-internal);
191202
}
192203

204+
.content-top {
205+
@apply min-w-0 max-w-full border-b;
206+
}
207+
208+
.content-bottom {
209+
@apply mt-auto box-border w-full justify-between border-t;
210+
}
211+
212+
.content-top:not(.header ~ .content-top) {
213+
@apply rounded-t;
214+
}
215+
216+
.content-bottom:not(.content-bottom ~ .footer),
193217
.content--no-footer {
194218
@apply rounded-b;
195219
}
@@ -287,10 +311,10 @@ slot[name="primary"] {
287311
}
288312

289313
:host([open][fullscreen]) {
290-
.header {
291-
border-radius: 0;
292-
}
293-
.footer {
314+
.header,
315+
.footer,
316+
.content-top,
317+
.content-bottom {
294318
border-radius: 0;
295319
}
296320
}
@@ -339,7 +363,8 @@ slot[name="primary"] {
339363
.modal {
340364
@apply border-0 border-t-4 border-solid;
341365
}
342-
.header {
366+
.header,
367+
.content-top {
343368
@apply rounded rounded-b-none;
344369
}
345370
}
@@ -348,10 +373,11 @@ slot[name="primary"] {
348373
* Tablet
349374
*/
350375
@media screen and (max-width: $viewport-medium) {
351-
@include slotted("header", "*") {
376+
@include slotted("header", "content-top", "*") {
352377
@apply text-1;
353378
}
354-
.footer {
379+
.footer,
380+
.content-bottom {
355381
@apply sticky bottom-0;
356382
}
357383
}
@@ -360,7 +386,8 @@ slot[name="primary"] {
360386
* Mobile
361387
*/
362388
@media screen and (max-width: $viewport-small) {
363-
.footer {
389+
.footer,
390+
.content-bottom {
364391
@apply flex-col;
365392
}
366393
.back,

src/components/modal/modal.stories.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const simple = (): string => html`
2727
>
2828
<h3 slot="header">Small Modal</h3>
2929
<div slot="content">
30-
<p>The small modal is perfect for short confirmation dialogs or very compact interfaces with few elements.</p>
30+
The small modal is perfect for short confirmation dialogs or very compact interfaces with few elements.
3131
</div>
3232
<calcite-button slot="back" kind="neutral" appearance="outline" icon="chevron-left" width="full"
3333
>Back</calcite-button
@@ -37,6 +37,34 @@ export const simple = (): string => html`
3737
</calcite-modal>
3838
`;
3939

40+
const mightyLongTextToScroll = html`
41+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non nisi et elit auctor aliquet ac suscipit eros. Sed nec
42+
nibh viverra, feugiat magna ut, posuere arcu. Curabitur varius erat ut suscipit convallis. Nullam semper pellentesque
43+
est laoreet accumsan. Aenean eget urna fermentum, porttitor dui et, tincidunt erat. Curabitur lacinia lacus in urna
44+
lacinia, ac interdum lorem fermentum. Ut accumsan malesuada varius. Lorem ipsum dolor sit amet, consectetur adipiscing
45+
elit. Phasellus tempus tempor magna, eu dignissim urna ornare non. Integer tempor justo blandit nunc ornare, a
46+
interdum nisl pharetra. Sed ultricies at augue vel fermentum. Maecenas laoreet odio lorem. Aliquam in pretium turpis.
47+
Donec quis felis a diam accumsan vehicula efficitur at orci. Donec sollicitudin gravida ultrices.
48+
`;
49+
50+
export const slots = (): string => html`
51+
<calcite-modal
52+
${boolean("open", true)}
53+
kind="${select("kind", ["brand", "danger", "info", "success", "warning"], "")}"
54+
scale="${select("scale", ["s", "m", "l"], "m")}"
55+
width="${select("width", ["s", "m", "l"], "s")}"
56+
${boolean("fullscreen", false)}
57+
${boolean("docked", false)}
58+
${boolean("escape-disabled", false)}
59+
>
60+
<h3 slot="header">Slot for a header.</h3>
61+
<div slot="content-top">Slot for a content-top.</div>
62+
<div slot="content" style="height: 100px">${mightyLongTextToScroll}</div>
63+
<div slot="content-bottom">Slot for a content-bottom.</div>
64+
<calcite-button slot="primary" width="full">Button</calcite-button>
65+
</calcite-modal>
66+
`;
67+
4068
export const darkModeRTLCustomSizeCSSVars_TestOnly = (): string => html`
4169
<calcite-modal
4270
class="calcite-mode-dark"

src/components/modal/modal.tsx

+39-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ import {
1717
connectConditionalSlotComponent,
1818
disconnectConditionalSlotComponent
1919
} from "../../utils/conditionalSlot";
20-
import { ensureId, focusFirstTabbable, getSlotted } from "../../utils/dom";
20+
import {
21+
ensureId,
22+
focusFirstTabbable,
23+
getSlotted,
24+
slotChangeHasAssignedElement
25+
} from "../../utils/dom";
2126
import {
2227
activateFocusTrap,
2328
connectFocusTrap,
@@ -50,6 +55,8 @@ import { ModalMessages } from "./assets/modal/t9n";
5055
/**
5156
* @slot header - A slot for adding header text.
5257
* @slot content - A slot for adding the component's content.
58+
* @slot contentTop - A slot for adding the component's content header.
59+
* @slot contentBottom - A slot for adding the component's content footer.
5360
* @slot primary - A slot for adding a primary button.
5461
* @slot secondary - A slot for adding a secondary button.
5562
* @slot back - A slot for adding a back button.
@@ -176,8 +183,8 @@ export class Modal
176183
connectedCallback(): void {
177184
this.mutationObserver?.observe(this.el, { childList: true, subtree: true });
178185
this.cssVarObserver?.observe(this.el, { attributeFilter: ["style"] });
179-
this.updateFooterVisibility();
180186
this.updateSizeCssVars();
187+
this.updateFooterVisibility();
181188
connectConditionalSlotComponent(this);
182189
connectLocalized(this);
183190
connectMessages(this);
@@ -223,6 +230,7 @@ export class Modal
223230
<slot name={CSS.header} />
224231
</header>
225232
</div>
233+
{this.renderContentTop()}
226234
<div
227235
class={{
228236
[CSS.content]: true,
@@ -232,6 +240,7 @@ export class Modal
232240
>
233241
<slot name={SLOTS.content} />
234242
</div>
243+
{this.renderContentBottom()}
235244
{this.renderFooter()}
236245
</div>
237246
</div>
@@ -255,6 +264,22 @@ export class Modal
255264
) : null;
256265
}
257266

267+
renderContentTop(): VNode {
268+
return (
269+
<div class={CSS.contentTop} hidden={!this.hasContentTop}>
270+
<slot name={SLOTS.contentTop} onSlotchange={this.contentTopSlotChangeHandler} />
271+
</div>
272+
);
273+
}
274+
275+
renderContentBottom(): VNode {
276+
return (
277+
<div class={CSS.contentBottom} hidden={!this.hasContentBottom}>
278+
<slot name={SLOTS.contentBottom} onSlotchange={this.contentBottomSlotChangeHandler} />
279+
</div>
280+
);
281+
}
282+
258283
renderCloseButton(): VNode {
259284
return !this.closeButtonDisabled ? (
260285
<button
@@ -349,6 +374,10 @@ export class Modal
349374

350375
@State() hasFooter = true;
351376

377+
@State() hasContentTop = false;
378+
379+
@State() hasContentBottom = false;
380+
352381
/**
353382
* We use internal variable to make sure initially open modal can transition from closed state when rendered
354383
*
@@ -534,4 +563,12 @@ export class Modal
534563
this.cssWidth = getComputedStyle(this.el).getPropertyValue("--calcite-modal-width");
535564
this.cssHeight = getComputedStyle(this.el).getPropertyValue("--calcite-modal-height");
536565
};
566+
567+
private contentTopSlotChangeHandler = (event: Event): void => {
568+
this.hasContentTop = slotChangeHasAssignedElement(event);
569+
};
570+
571+
private contentBottomSlotChangeHandler = (event: Event): void => {
572+
this.hasContentBottom = slotChangeHasAssignedElement(event);
573+
};
537574
}

src/components/modal/resources.ts

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const CSS = {
1313
container: "container",
1414
content: "content",
1515
contentNoFooter: "content--no-footer",
16+
contentBottom: "content-bottom",
17+
contentTop: "content-top",
1618
slottedInShell: "slotted-in-shell",
1719

1820
// these classes help apply the animation in phases to only set transform on open/close
@@ -36,6 +38,8 @@ export const ICONS = {
3638

3739
export const SLOTS = {
3840
content: "content",
41+
contentBottom: "content-bottom",
42+
contentTop: "content-top",
3943
header: "header",
4044
back: "back",
4145
secondary: "secondary",

0 commit comments

Comments
 (0)