Skip to content

Commit

Permalink
Add custom footer slot for Modal component (#1786)
Browse files Browse the repository at this point in the history
Co-authored-by: HQ\palak.chaudhary <palak.chaudhary@transporeon.com>
  • Loading branch information
pchaudh18 and palakchaudhary18 authored Oct 19, 2023
1 parent aeaa532 commit e96b5a1
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe('modus-modal', () => {

await page.setContent('<modus-modal header-text="Header Text"></modus-modal>');
const component = await page.find('modus-modal');
const element = await page.find('modus-modal >>> div.header');
const element = await page.find('modus-modal >>> div');
expect(element.innerText).toContain('Header Text');

component.setProperty('headerText', 'New Text');
Expand Down
17 changes: 8 additions & 9 deletions stencil-workspace/src/components/modus-modal/modus-modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
min-width: 300px;
outline: 0;

.header {
header {
align-items: center;
border-bottom: 1px solid $modus-modal-divider-color;
display: flex;
Expand Down Expand Up @@ -61,16 +61,15 @@
padding: $rem-16px;
}

.footer {
align-items: center;
footer {
border-top: 1px solid $modus-modal-divider-color;
display: flex;
height: 64px;
justify-content: flex-end;
padding: 0 $rem-16px;
padding: $rem-20px $rem-16px;

modus-button:first-of-type {
margin: 0 $rem-8px;
&.has-buttons {
align-items: center;
display: flex;
gap: $rem-8px;
justify-content: flex-end;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('modus-modal', () => {
<div class="modus-modal overlay hidden" role="dialog" style="z-index: 1;">
<div class="content">
<div id="startTrap" tabindex="0" aria-hidden="true"></div>
<div class="header">
<header>
<div role="button" tabindex="0" aria-label="Close">
<svg class="icon-close" height="20" width="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
Expand All @@ -26,11 +26,13 @@ describe('modus-modal', () => {
</defs>
</svg>
</div>
</div>
</header>
<div class="body">
<slot></slot>
</div>
<div class="footer"></div>
<footer>
<slot name="footerContent"></slot>
</footer>
<div id="endTrap" tabindex="0" aria-hidden="true"></div>
</div>
</div>
Expand Down
60 changes: 35 additions & 25 deletions stencil-workspace/src/components/modus-modal/modus-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
import { Component, Element, Event, EventEmitter, h, JSX, Listen, Method, Prop, State } from '@stencil/core';
import { IconClose } from '../icons/icon-close';
import { FocusWrap, ModalFocusWrapping } from './modal-focus-wrapping';
import { Fragment } from '@stencil/core/internal';

/**
* @slot footerContent - Slot for a custom footer content
*/
@Component({
tag: 'modus-modal',
styleUrl: 'modus-modal.scss',
Expand Down Expand Up @@ -149,7 +153,7 @@ export class ModusModal {

renderModalHeader(): JSX.Element[] {
return (
<div class="header">
<header>
{this.headerText}
<div
role="button"
Expand All @@ -159,35 +163,41 @@ export class ModusModal {
onKeyDown={(event) => this.handleCloseKeydown(event)}>
<IconClose size="20" />
</div>
</div>
</header>
);
}

renderModalFooter(): JSX.Element[] {
return (
<div class="footer">
{this.secondaryButtonText && (
<modus-button
disabled={this.secondaryButtonDisabled}
button-style="outline"
color="secondary"
ariaLabel={this.secondaryButtonAriaLabel}
onButtonClick={() => this.secondaryButtonClick.emit()}
onKeyDown={(event) => this.handlePrimaryKeydown(event)}>
{this.secondaryButtonText}
</modus-button>
)}
{this.primaryButtonText && (
<modus-button
disabled={this.primaryButtonDisabled}
color="primary"
ariaLabel={this.primaryButtonAriaLabel}
onButtonClick={() => this.primaryButtonClick.emit()}
onKeyDown={(event) => this.handleSecondaryKeydown(event)}>
{this.primaryButtonText}
</modus-button>
)}
</div>
<Fragment>
<footer
class={{
'has-buttons': Boolean(this.primaryButtonText || this.secondaryButtonText),
}}>
{this.secondaryButtonText && (
<modus-button
disabled={this.secondaryButtonDisabled}
button-style="outline"
color="secondary"
ariaLabel={this.secondaryButtonAriaLabel}
onButtonClick={() => this.secondaryButtonClick.emit()}
onKeyDown={(event) => this.handlePrimaryKeydown(event)}>
{this.secondaryButtonText}
</modus-button>
)}
{this.primaryButtonText && (
<modus-button
disabled={this.primaryButtonDisabled}
color="primary"
ariaLabel={this.primaryButtonAriaLabel}
onButtonClick={() => this.primaryButtonClick.emit()}
onKeyDown={(event) => this.handleSecondaryKeydown(event)}>
{this.primaryButtonText}
</modus-button>
)}
<slot name="footerContent"></slot>
</footer>
</Fragment>
);
}

Expand Down
7 changes: 7 additions & 0 deletions stencil-workspace/src/components/modus-modal/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ Type: `Promise<void>`



## Slots

| Slot | Description |
| ----------------- | -------------------------------- |
| `"footerContent"` | Slot for a custom footer content |


## Dependencies

### Depends on
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { Story } from '@storybook/addon-docs';

[Modus Dialog](https://modus.trimble.com/components/web/modals/) web components interrupt workflows when user interaction is required. They are referenced using the `<modus-modal>` custom HTML element.

This component utilizes the slot element, allowing you to render your own HTML in the content portion of the modal.
This modal component uses the slot element to render the content and the slot `footerContent` to render custom footer content.

When designing a custom footer, it is highly recommended to use button progressions from, structural and color progression. Use only one button progression in a single product, for example, `primary` button should be followed by a `secondary` button or `tertiary` button not the outline buttons. For more guidelines refer to [Modus Styleguide](https://modus.trimble.com/components/web/modals/styles/).

### Default

Expand All @@ -23,6 +25,36 @@ This component utilizes the slot element, allowing you to render your own HTML i
<p>Woo-hoo, you're reading this text in a modal!</p>
</modus-modal>

<script>
document.querySelector('#btn-modal').addEventListener('buttonClick', () => {
document.querySelector('modus-modal').open();
});
document.querySelector('modus-modal').addEventListener('closed', () => {
// Timeout is a workaround for Stencil Web Component not capturing the state updates quick enough when another component is immediately focussed
setTimeout(() => {
document.querySelector('#btn-modal').focusButton();
}, 100);
});
</script>
```
### Custom footer

<Story id="components-modal--custom-footer" height={'400px'} />

```html
<modus-button id="btn-modal" color="primary">Open modal</modus-button>
<modus-modal>
<p>A dialog or a modal is a window overlaid on the primary window. It interrupts the user and requires an action. It disables the main content until the user explicitly interacts with the modal dialog.</p>
<div style="align-items: center;
display: flex;
justify-content: flex-end; gap: 8px; height:100%;" slot="footerContent">
<modus-button color="tertiary">Cancel</modus-button>
<modus-button color="secondary">Check later</modus-button>
<modus-button color="primary">Approve</modus-button>
</div>
</modus-modal>

<script>
document.querySelector('#btn-modal').addEventListener('buttonClick', () => {
document.querySelector('modus-modal').open();
Expand Down Expand Up @@ -70,6 +102,12 @@ This component utilizes the slot element, allowing you to render your own HTML i
| `close` | Closes the Modal | | `Promise<void>` |
| `open` | Opens the Modal | | `Promise<void>` |

### Slots

| Slot | Description |
| ----------------- | -------------------------------- |
| `"footerContent"` | Slot for a custom footer content |

### Accessibility

- Modal has `role` of `dialog`.
Expand All @@ -78,3 +116,4 @@ This component utilizes the slot element, allowing you to render your own HTML i
- When Secondary button has focus, **Enter** emits `secondaryButtonClick` event.
- When the Modal is open, tabbing can be performed only on the focussable elements inside Modal. Note: Pressing `Shift` + `Tab` on Modal Header cannot go back to Modal Footer.
- Pressing `Escape` key closes the Modal and emits `close` event.
- To customize footer, slot option is provided and referenced by `slot='footer'`.
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default {
},
parameters: {
actions: {
handles: ['closed', 'primaryButtonClick', 'secondaryButtonClick'],
handles: ['closed', 'primaryButtonClick', 'secondaryButtonClick', 'buttonClick'],
},
docs: {
inlineStories: false,
Expand All @@ -95,15 +95,15 @@ export default {
const Template = ({ ariaLabel, headerText, primaryButtonAriaLabel,primaryButtonDisabled, primaryButtonText, secondaryButtonAriaLabel, secondaryButtonDisabled, secondaryButtonText, zIndex, backdrop }) => html`
<modus-button id="btn-modal" color="primary">Open modal</modus-button>
<modus-modal
aria-label=${ariaLabel}
header-text=${headerText}
primary-button-aria-label=${primaryButtonAriaLabel}
primary-button-disabled=${primaryButtonDisabled}
primary-button-text=${primaryButtonText}
secondary-button-aria-label=${secondaryButtonAriaLabel} secondary-button-disabled=${secondaryButtonDisabled}
secondary-button-text=${secondaryButtonText}
z-index=${zIndex}
backdrop=${backdrop}>
aria-label=${ariaLabel}
header-text=${headerText}
primary-button-aria-label=${primaryButtonAriaLabel}
primary-button-disabled=${primaryButtonDisabled}
primary-button-text=${primaryButtonText}
secondary-button-aria-label=${secondaryButtonAriaLabel} secondary-button-disabled=${secondaryButtonDisabled}
secondary-button-text=${secondaryButtonText}
z-index=${zIndex}
backdrop=${backdrop}>
<p>Woo-hoo, you're reading this text in a modal!</p>
</modus-modal>
${setScript()}
Expand All @@ -121,6 +121,31 @@ Default.args = {
zIndex: '1',
backdrop: 'default'};

const CustomFooterTemplate = ({ ariaLabel, headerText, zIndex, backdrop }) => html`
<modus-button id="btn-modal" color="primary">Open modal</modus-button>
<modus-modal
aria-label=${ariaLabel}
header-text=${headerText}
z-index=${zIndex}
backdrop=${backdrop}>
<p>A dialog or a modal is a window overlaid on the primary window. It interrupts the user and requires an action. It disables the main content until the user explicitly interacts with the modal dialog.</p>
<div style="align-items: center;
display: flex;
justify-content: flex-end; gap: 8px; height:100%;" slot="footerContent">
<modus-button color="tertiary">Cancel</modus-button>
<modus-button color="secondary">Check later</modus-button>
<modus-button color="primary">Approve</modus-button>
</div>
</modus-modal>
${setScript()}
`;
export const CustomFooter = CustomFooterTemplate.bind({});
CustomFooter.args = {
ariaLabel: 'Modal',
headerText: 'Modal title',
zIndex: '1',
backdrop: 'default'};

const setScript = () => {
const tag = document.createElement('script');
tag.innerHTML = `
Expand Down

0 comments on commit e96b5a1

Please sign in to comment.