Skip to content

Commit

Permalink
Set up ModalMultiplexer and ModalVisibilityStore
Browse files Browse the repository at this point in the history
  • Loading branch information
seancolsen committed Nov 17, 2021
1 parent 8e022b3 commit 279aaa9
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 44 deletions.
1 change: 1 addition & 0 deletions mathesar_ui/src/component-library/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ export { default as Pagination } from './pagination/Pagination.svelte';
export { default as Dropdown } from './dropdown/Dropdown.svelte';
export { default as Select } from './select/Select.svelte';
export { default as FileUpload } from './file-upload/FileUpload.svelte';
export * from './modal/ModalMultiplexer';
export { default as Modal } from './modal/Modal.svelte';
47 changes: 17 additions & 30 deletions mathesar_ui/src/component-library/modal/Modal.svelte
Original file line number Diff line number Diff line change
@@ -1,56 +1,43 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import type { Size } from '@mathesar-component-library/types';
import { portal } from '@mathesar-component-library';
import type { ModalVisibilityStore } from './ModalVisibilityStore';
import type { ModalCloseAction } from './modal';
const dispatch = createEventDispatcher();
// Additional classes
export let isOpen: ModalVisibilityStore;
export let title: string;
let classes = '';
export { classes as class };
// Inline styles
export let style = '';
// Size
export let size: Size = 'medium';
// Boolean to open/close modal
export let isOpen = true;
// Close when esc key is pressed
export let closeOnEsc = true;
export let closeOn: ModalCloseAction[] = ['button', 'esc', 'overlay'];
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape' && isOpen && closeOnEsc) {
isOpen = false;
if (event.key === 'Escape' && $isOpen && closeOn.includes('esc')) {
$isOpen = false;
}
}
function dispatchOpenEvent(_isOpen: boolean) {
if (_isOpen) {
dispatch('open');
} else {
dispatch('close');
}
}
$: dispatchOpenEvent(isOpen);
</script>

<svelte:window on:keydown={handleKeydown}/>

{#if isOpen}
{#if $isOpen}
<div class="modal-wrapper" use:portal>
<div class={['modal', `modal-size-${size}`, classes].join(' ')} {style}>

{#if $$slots.title || title}
<div class="title">
{#if $$slots.title}<slot name="title"/>{:else}{title}{/if}
</div>
{/if}

<div class="body">
<slot/>
</div>

{#if $$slots.footer}
<div class="footer">
<slot name="footer"/>
</div>
<div class="footer"><slot name="footer"/></div>
{/if}
</div>
</div>
Expand Down
28 changes: 28 additions & 0 deletions mathesar_ui/src/component-library/modal/ModalMultiplexer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { writable } from 'svelte/store';
import { ModalVisibilityStore } from './ModalVisibilityStore';

/**
* Opens/closes modals, ensuring that only one modal is open at a time.
*/
export class ModalMultiplexer {
openModalId = writable<number | undefined>(undefined);

private maxId = 0;

private getId(): number {
this.maxId += 1;
return this.maxId;
}

open(modalId: number): void {
this.openModalId.set(modalId);
}

close(): void {
this.openModalId.set(undefined);
}

createVisibilityStore(): ModalVisibilityStore {
return new ModalVisibilityStore({ id: this.getId(), multiplexer: this });
}
}
65 changes: 65 additions & 0 deletions mathesar_ui/src/component-library/modal/ModalVisibilityStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type {
Subscriber, Unsubscriber, Writable, Updater,
} from 'svelte/store';
import type { ModalMultiplexer } from './ModalMultiplexer';

/**
* This store acts somewhat like a radio button interface to the
* ModalMultiplexer. You can read from this store to determine whether a
* specific modal is open. And when you write to this store, that write will get
* passed up to the ModalMultiplexer which ultimately sets the value, ensuring
* that only one modal is open at a time.
*/
export class ModalVisibilityStore implements Writable<boolean> {
id: number;

multiplexer: ModalMultiplexer;

constructor({
id,
multiplexer,
}: {
id: number,
multiplexer: ModalMultiplexer,
}) {
this.id = id;
this.multiplexer = multiplexer;
}

subscribe(subscription: Subscriber<boolean>): Unsubscriber {
return this.multiplexer.openModalId.subscribe((openModalId) => {
subscription(openModalId === this.id);
});
}

update(updater: Updater<boolean>): void {
this.multiplexer.openModalId.update((openModalId) => {
const isCurrentlyVisible = this.id === openModalId;
const shouldBecomeVisible = updater(isCurrentlyVisible);
if (shouldBecomeVisible) {
return this.id;
}
return undefined;
});
}

set(isVisible: boolean): void {
if (isVisible) {
this.multiplexer.open(this.id);
} else {
this.multiplexer.close();
}
}

open(): void {
this.set(true);
}

close(): void {
this.set(false);
}

toggle(): void {
this.update((isVisible) => !isVisible);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,38 @@
import { Meta, Story } from '@storybook/addon-svelte-csf';
import { Button } from '@mathesar-component-library';
import Modal from '../Modal.svelte';
import { ModalMultiplexer } from '../ModalMultiplexer';
const meta = {
title: 'Components/Modal',
component: Modal,
title: 'Systems/Modal',
};
let isBasicModalOpen = false;
const modal = new ModalMultiplexer();
const exampleA = modal.createVisibilityStore();
const exampleB = modal.createVisibilityStore();
</script>

<Meta {...meta} />

<Story
name="Basic">
<Button on:click={() => { isBasicModalOpen = true; }}>Open Modal</Button>
<Modal bind:isOpen={isBasicModalOpen}>
Render anything in the slot!
<svelte:fragment slot="footer">
Render anything in the footer slot!
<Button on:click={() => { isBasicModalOpen = false; }}>
Close
</Button>
</svelte:fragment>
<Story name="Basic">
<Button on:click={() => exampleA.open()}>Open Modal A</Button>
<Button on:click={() => exampleB.open()}>Open Modal B</Button>

<Modal isOpen={exampleA} title="Modal A">
Here is modal content

<div slot="footer">
<p>This is the modal footer</p>
<p><Button on:click={() => exampleA.close()}>Close</Button></p>
</div>
</Modal>


<Modal isOpen={exampleB} title="Modal B">
<p>Here is modal content</p>

<Button on:click={() => exampleA.open()}>Open Modal A instead (Modal B will close)</Button>
</Modal>

</Story>
1 change: 1 addition & 0 deletions mathesar_ui/src/component-library/modal/modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type ModalCloseAction = 'button' | 'esc' | 'overlay';

0 comments on commit 279aaa9

Please sign in to comment.