Skip to content

Commit

Permalink
feat(shell): Add slots for Modal and Alert (#5983)
Browse files Browse the repository at this point in the history
**Related Issue:** 

Proposed as part of Shell / Layout enhancements:
https://confluencewikidev.esri.com/pages/viewpage.action?pageId=222267264
(VPN needed)

## Summary
This is a draft PR to add two new slots to `calcite-shell`: `modal` and
`alerts`.

This pattern was proposed as part of our upcoming Shell / layout
improvements, and would be leveraged by the upcoming proposed
`calcite-sheet` component.

When expected components are slotted into named slots, conditional
styles are applied to constrain their extent to that of the
`calcite-shell` (including covering the existing `header` and `footer`
slots). This is useful in many cases, specifically when the
`calcite-shell` is embedded amongst other content, like tutorials,
documentation pages, or any place the user experience is not a
completely full-screen `calcite-shell`.

When these components are _not_ slotted into the new `calcite-shell`
slots - their behavior remains unchanged.



A demo is available locally at
`/demos/shell/basic-slotted-elements.html`

cc @asangma @mitc7862 

Screen recording of proposed behavior: 



https://user-images.githubusercontent.com/4733155/207209472-000f38d5-d44c-4ead-a277-dd3fc5ef905c.mov
  • Loading branch information
macandcheese committed Dec 16, 2022
1 parent 3edc2ae commit d824bf7
Show file tree
Hide file tree
Showing 14 changed files with 387 additions and 64 deletions.
8 changes: 8 additions & 0 deletions src/components/alert/alert.scss
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,11 @@ $alertDurations: "fast" 6000ms, "medium" 10000ms, "slow" 14000ms;
@apply w-full opacity-100;
}
}

/**
* Conditional styles for when Alert is slotted in Shell
*/

.container.slotted-in-shell {
position: absolute;
}
12 changes: 11 additions & 1 deletion src/components/alert/alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@ export class Alert implements OpenCloseComponent, LoadableComponent, T9nComponen
/* wired up by t9n util */
}

/**
* This internal property, managed by a containing calcite-shell, is used
* to inform the component if special configuration or styles are needed
*
* @internal
*/
@Prop({ mutable: true }) slottedInShell: boolean;

@Watch("icon")
@Watch("kind")
updateRequestedIcon(): void {
Expand Down Expand Up @@ -195,6 +203,7 @@ export class Alert implements OpenCloseComponent, LoadableComponent, T9nComponen
disconnectOpenCloseComponent(this);
disconnectLocalized(this);
disconnectMessages(this);
this.slottedInShell = false;
}

render(): VNode {
Expand Down Expand Up @@ -251,7 +260,8 @@ export class Alert implements OpenCloseComponent, LoadableComponent, T9nComponen
class={{
container: true,
queued,
[placement]: true
[placement]: true,
[CSS.slottedInShell]: this.slottedInShell
}}
onPointerOut={this.autoClose && this.autoCloseTimeoutId ? this.handleMouseLeave : null}
onPointerOver={this.autoClose && this.autoCloseTimeoutId ? this.handleMouseOver : null}
Expand Down
3 changes: 2 additions & 1 deletion src/components/alert/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export const SLOTS = {
export const CSS = {
actionsEnd: "actions-end",
container: "container",
close: "alert-close"
close: "alert-close",
slottedInShell: "slotted-in-shell"
};
14 changes: 13 additions & 1 deletion src/components/modal/modal.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,19 @@ describe("calcite-modal accessibility checks", () => {
expect(documentClass).toEqual(true);
});

it("correctly removes overflow class on document when open", async () => {
it("correctly does not add overflow class on document when open and slotted in shell modals slot", async () => {
const page = await newE2EPage();
await page.setContent(`<calcite-shell><calcite-modal slot="modals"></calcite-modal></calcite-shell>`);
const modal = await page.find("calcite-modal");
await modal.setProperty("open", true);
await page.waitForChanges();
const documentClass = await page.evaluate(() => {
return document.documentElement.classList.contains("overflow-hidden");
});
expect(documentClass).toEqual(false);
});

it("correctly removes overflow class on document once closed", async () => {
const page = await newE2EPage();
await page.setContent(`<calcite-modal></calcite-modal>`);
const modal = await page.find("calcite-modal");
Expand Down
40 changes: 30 additions & 10 deletions src/components/modal/modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
*/

:host {
@apply absolute flex inset-0 opacity-0 z-overlay;
visibility: hidden !important;
transition: visibility 0ms linear var(--calcite-internal-animation-timing-slow),
opacity var(--calcite-internal-animation-timing-slow) $easing-function;
}

.container {
@apply text-color-2
fixed
inset-0
Expand Down Expand Up @@ -106,6 +113,12 @@
@apply opacity-100;
visibility: visible !important;
transition-delay: 0ms;
}

:host([open]) .container {
@apply opacity-100;
visibility: visible !important;
transition-delay: 0ms;
.modal--open {
@apply pointer-events-auto visible opacity-100;
transition: transform var(--calcite-internal-animation-timing-slow) $easing-function, visibility 0ms linear,
Expand Down Expand Up @@ -190,10 +203,8 @@
font-size: var(--calcite-modal-content-text, var(--calcite-modal-context-text-internal));
}

:host([background-color="grey"]) {
.content {
@apply bg-background;
}
:host([background-color="grey"]) .content {
@apply bg-background;
}

/**
Expand Down Expand Up @@ -239,10 +250,8 @@ slot[name="primary"] {
* Sizes
*/
@mixin modal-size($size, $width) {
:host([width="#{$size}"]) {
.modal {
max-inline-size: $width;
}
:host([width="#{$size}"]) .modal {
max-inline-size: $width;
}
@media screen and (max-width: $width + 2 * $baseline) {
:host([width="#{$size}"]) {
Expand All @@ -254,7 +263,7 @@ slot[name="primary"] {
max-block-size: unset;
}
}
:host([width="#{$size}"][docked]) {
:host([width="#{$size}"][docked]) .container {
@apply items-end;
}
}
Expand All @@ -272,7 +281,6 @@ slot[name="primary"] {
* Fullscreen
*/
:host([fullscreen]) {
background-color: transparent;
.modal {
@apply m-0 h-full max-h-full w-full max-w-full;
--calcite-modal-hidden-position: translate3D(0, 20px, 0) scale(0.95);
Expand Down Expand Up @@ -374,3 +382,15 @@ slot[name="primary"] {
@apply m-0 mb-1;
}
}

/**
* Conditional styles for when Modal is slotted in Shell
*/

.container.slotted-in-shell {
position: absolute;
pointer-events: auto;
calcite-scrim {
position: absolute;
}
}
55 changes: 34 additions & 21 deletions src/components/modal/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ export class Modal
/* wired up by t9n util */
}

/**
* This internal property, managed by a containing calcite-shell, is used
* to inform the component if special configuration or styles are needed
*
* @internal
*/
@Prop({ mutable: true }) slottedInShell: boolean;

//--------------------------------------------------------------------------
//
// Lifecycle
Expand Down Expand Up @@ -184,6 +192,7 @@ export class Modal
deactivateFocusTrap(this);
disconnectLocalized(this);
disconnectMessages(this);
this.slottedInShell = false;
}

render(): VNode {
Expand All @@ -194,31 +203,33 @@ export class Modal
aria-modal="true"
role="dialog"
>
<calcite-scrim class={CSS.scrim} onClick={this.handleOutsideClose} />
{this.renderStyle()}
<div
class={{
[CSS.modal]: true,
[CSS.modalOpen]: this.isOpen
}}
ref={this.setTransitionEl}
>
<div class={CSS.header}>
{this.renderCloseButton()}
<header class={CSS.title}>
<slot name={CSS.header} />
</header>
</div>
<div class={{ [CSS.container]: true, [CSS.slottedInShell]: this.slottedInShell }}>
<calcite-scrim class={CSS.scrim} onClick={this.handleOutsideClose} />
{this.renderStyle()}
<div
class={{
content: true,
"content--no-footer": !this.hasFooter
[CSS.modal]: true,
[CSS.modalOpen]: this.isOpen
}}
ref={(el) => (this.modalContent = el)}
ref={this.setTransitionEl}
>
<slot name={SLOTS.content} />
<div class={CSS.header}>
{this.renderCloseButton()}
<header class={CSS.title}>
<slot name={CSS.header} />
</header>
</div>
<div
class={{
content: true,
"content--no-footer": !this.hasFooter
}}
ref={(el) => (this.modalContent = el)}
>
<slot name={SLOTS.content} />
</div>
{this.renderFooter()}
</div>
{this.renderFooter()}
</div>
</Host>
);
Expand Down Expand Up @@ -469,7 +480,9 @@ export class Modal
this.titleId = ensureId(titleEl);
this.contentId = ensureId(contentEl);

document.documentElement.classList.add(CSS.overflowHidden);
if (!this.slottedInShell) {
document.documentElement.classList.add(CSS.overflowHidden);
}
}

handleOutsideClose = (): void => {
Expand Down
2 changes: 2 additions & 0 deletions src/components/modal/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export const CSS = {
secondary: "secondary",
primary: "primary",
overflowHidden: "overflow-hidden",
container: "container",
slottedInShell: "slotted-in-shell",

// these classes help apply the animation in phases to only set transform on open/close
// this helps avoid a positioning issue for any floating-ui-owning children
Expand Down
8 changes: 6 additions & 2 deletions src/components/shell/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ export const CSS = {
main: "main",
content: "content",
contentBehind: "content--behind",
footer: "footer"
footer: "footer",
positionedSlotWrapper: "positioned-slot-wrapper",
container: "container"
};

export const SLOTS = {
centerRow: "center-row",
panelStart: "panel-start",
panelEnd: "panel-end",
header: "header",
footer: "footer"
footer: "footer",
alerts: "alerts",
modals: "modals"
};
12 changes: 0 additions & 12 deletions src/components/shell/shell.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,6 @@ describe("calcite-shell", () => {

it("honors hidden attribute", async () => hidden("calcite-shell"));

it("defaults", async () => {
const page = await newE2EPage();

await page.setContent("<calcite-shell></calcite-shell>");

const footer = await page.find(`calcite-shell >>> slot[name="${SLOTS.footer}"]`);
const header = await page.find(`calcite-shell >>> slot[name="${SLOTS.header}"]`);

expect(footer).toBeNull();
expect(header).toBeNull();
});

it("has slots", () => slots("calcite-shell", SLOTS));

it("content node should always be present", async () => {
Expand Down
7 changes: 7 additions & 0 deletions src/components/shell/shell.scss
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,10 @@ slot[name="center-row"]::slotted(calcite-shell-center-row:not([detached])) {
inset-block-end: theme("spacing.2");
inset-inline: var(--calcite-shell-tip-spacing);
}

// positioning logic for expected slotted components
.position-wrapper {
position: absolute;
pointer-events: none;
inset: 0;
}
78 changes: 78 additions & 0 deletions src/components/shell/shell.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,84 @@ background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
<footer slot="footer">My Shell Footer</footer>
</calcite-shell>`;

export const slottedModalAndAlert = (): string => html` <main>
<p class="padded-content">
<calcite-notice width="full" open><span slot="title">Other page content outside of shell</span></calcite-notice>
Master cleanse occupy lo-fi meh. Green juice williamsburg XOXO man bun ascot fit. Knausgaard heirloom four dollar
toast DSA chicharrones, typewriter chia raw denim. Bicycle rights mustache humblebrag, mixtape slow-carb retro
vibecession franzen chia. Bespoke coloring book hot chicken literally bushwick succulents wayfarers. Dreamcatcher
taiyaki celiac pork belly migas, fashion axe beard shabby chic. Forage chia twee bushwick readymade yuccie praxis
enamel pin cred mukbang bicycle rights VHS iPhone pour-over subway tile.
</p>
<calcite-shell
style="
width:100%;
height:500px;
max-height:80%;
position:relative;
"
>
<div class="gnav" slot="header">Header Example</div>
<calcite-modal open slot="modals" docked><span slot="header">Modal slotted in Shell</span></calcite-modal>
<calcite-alert open slot="alerts" placement="top-end"
><span slot="title">Alert slotted in Shell</span>
</calcite-alert>
<calcite-shell-panel id="primary-panel" slot="panel-start" position="start">
<calcite-action-bar slot="action-bar">
<calcite-action-group>
<calcite-action text="Save" icon="save" indicator> </calcite-action>
<calcite-action text-enabled icon="map" text="New" slot="menu-actions"> </calcite-action>
<calcite-action text-enabled icon="collection" text="Open" slot="menu-actions"> </calcite-action>
</calcite-action-group>
<calcite-action-group>
<calcite-action icon="layers" text="Layers" active> </calcite-action>
<calcite-action icon="basemap" text="Basemaps"> </calcite-action>
<calcite-action icon="legend" text="Legend"> </calcite-action>
<calcite-action icon="bookmark" text="Bookmarks"> </calcite-action>
</calcite-action-group>
</calcite-action-bar>
<calcite-panel heading="Panel">
<div class="padded-content">Panel content<br />Padding is fake.</div>
</calcite-panel>
</calcite-shell-panel>
<calcite-shell-panel slot="panel-end" position="end">
<calcite-action-bar slot="action-bar">
<calcite-tooltip slot="expand-tooltip" label="tooltip" disable-pointer>Add layers</calcite-tooltip>
<calcite-action-group>
<calcite-action text="Layer properties" icon="sliders-horizontal"> </calcite-action>
<calcite-action text="Styles" icon="shapes"> </calcite-action>
<calcite-action text="Filter" icon="layer-filter"> </calcite-action>
<calcite-action text="Configure pop-ups" icon="popup" active> </calcite-action>
<calcite-action text-enabled text="Configure attributes" icon="feature-details" slot="menu-actions">
</calcite-action>
<calcite-action text-enabled text="Labels" icon="label" slot="menu-actions"> </calcite-action>
<calcite-action text-enabled text="Tablew" icon="table" slot="menu-actions"> </calcite-action>
</calcite-action-group>
</calcite-action-bar>
<calcite-flow>
<calcite-flow-item heading="Flow 01">
<div class="padded-content">Flow 01 content<br />Padding is fake.</div>
</calcite-flow-item>
<calcite-flow-item heading="Flow 02">
<div class="padded-content">Flow 02 content<br />Padding is fake.</div>
</calcite-flow-item>
</calcite-flow>
</calcite-shell-panel>
<calcite-panel heading="Main content">
<div class="padded-content">The borders are only applied to "known" components.<br />Padding is fake.</div>
</calcite-panel>
<footer slot="footer">Footer Example</footer>
</calcite-shell>
<p class="padded-content">
<calcite-notice width="full" open><span slot="title">Notice outside of shell</span></calcite-notice>
Edison bulb iceland narwhal fit DSA. Activated charcoal dreamcatcher shabby chic, microdosing gluten-free locavore
chambray tumblr hella sus ugh cronut tofu. Vibecession air plant etsy, vape church-key narwhal activated charcoal
offal kombucha hella. Actually mumblecore butcher, iceland man bun prism blog taiyaki roof party portland hashtag.
</p>
</main>`;

export const contentBehind = (): string => html`<calcite-shell content-behind>
${headerHTML}
<calcite-shell-panel slot="panel-start">${leadingPanelHTML}</calcite-shell-panel>
Expand Down
Loading

0 comments on commit d824bf7

Please sign in to comment.