Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e7bbcb0

Browse files
authoredNov 8, 2023
Replace Modal.isOpen with controlled boolean (#422)
1 parent 53fe0d6 commit e7bbcb0

File tree

6 files changed

+169
-129
lines changed

6 files changed

+169
-129
lines changed
 

‎packages/core/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@viamrobotics/prime-core",
3-
"version": "0.0.58",
3+
"version": "0.0.59",
44
"publishConfig": {
55
"access": "public"
66
},
+43-25
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,81 @@
1-
import { describe, it, expect, beforeEach } from 'vitest';
1+
import { describe, it, expect, vi } from 'vitest';
22
import { render, screen, within } from '@testing-library/svelte';
33
import userEvent from '@testing-library/user-event';
4+
import type { ComponentProps } from 'svelte';
45
import Modal from '../modal.svelte';
5-
import { writable, get } from 'svelte/store';
66

77
describe('Modal', () => {
8-
let isOpen = writable(true);
8+
const onClose = vi.fn();
99

10-
beforeEach(() => {
11-
isOpen = writable(true);
10+
const renderSubject = (props: ComponentProps<Modal>) => {
11+
const { component } = render(Modal, props);
12+
component.$on('close', onClose);
13+
};
14+
15+
it('should be visible if open is true ', () => {
16+
renderSubject({ isOpen: true });
17+
18+
const modal = screen.queryByRole('dialog');
19+
20+
expect(modal).toBeInTheDocument();
21+
expect(modal).toHaveAttribute('aria-modal', 'true');
22+
expect(onClose).not.toHaveBeenCalled();
23+
});
24+
25+
it('should not be visible if open is false', () => {
26+
renderSubject({ isOpen: false });
27+
28+
const modal = screen.queryByRole('dialog');
29+
30+
expect(modal).not.toBeInTheDocument();
31+
expect(onClose).not.toHaveBeenCalled();
1232
});
1333

1434
it('should close modal when close icon button is clicked', async () => {
15-
render(Modal, { isOpen });
1635
const user = userEvent.setup();
36+
renderSubject({ isOpen: true });
1737

1838
const modal = screen.getByRole('dialog');
1939
const closeButton = within(modal).getByRole('button', { name: /close/iu });
2040

2141
await user.click(closeButton);
22-
23-
expect(get(isOpen)).toBe(false);
42+
expect(onClose).toHaveBeenCalledOnce();
2443
});
2544

2645
it('should close modal when clicked outside the modal', async () => {
27-
render(Modal, { isOpen });
2846
const user = userEvent.setup();
47+
renderSubject({ isOpen: true });
2948

3049
const modal = screen.getByRole('dialog');
3150
await user.click(modal.parentElement!);
3251

33-
expect(get(isOpen)).toBe(false);
52+
expect(onClose).toHaveBeenCalledOnce();
3453
});
3554

36-
it('if open is true, modal should be visible', () => {
37-
render(Modal, { isOpen });
38-
const modal = screen.queryByRole('dialog');
39-
expect(modal).toBeInTheDocument();
40-
expect(modal).toHaveAttribute('aria-modal', 'true');
41-
});
55+
it('should close modal when escape key is pressed', async () => {
56+
const user = userEvent.setup();
57+
renderSubject({ isOpen: true });
4258

43-
it('if open is false, modal should not be visible', () => {
44-
isOpen.set(false);
45-
render(Modal, { isOpen });
46-
const modal = screen.queryByRole('dialog');
47-
expect(modal).not.toBeInTheDocument();
59+
await user.keyboard('{Escape}');
60+
61+
expect(onClose).toHaveBeenCalledOnce();
4862
});
4963

50-
it('should close modal when escape key is pressed', async () => {
51-
render(Modal, { isOpen });
64+
it('should not emit close events on escape if the modal is closed', async () => {
5265
const user = userEvent.setup();
66+
renderSubject({ isOpen: false });
67+
5368
await user.keyboard('{Escape}');
54-
expect(get(isOpen)).toBe(false);
69+
70+
expect(onClose).not.toHaveBeenCalled();
5571
});
5672

5773
it('should focus on heading element on mount', () => {
58-
render(Modal, { isOpen });
74+
render(Modal, { isOpen: true });
75+
5976
const modal = screen.getByRole('dialog');
6077
const heading = within(modal).getByRole('heading');
78+
6179
expect(heading).toHaveFocus();
6280
});
6381
});

‎packages/core/src/lib/modal.svelte

+48-30
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,62 @@
44
Creates a modal overlay.
55
66
```svelte
7-
<div>
8-
<Button on:click={handleOpenModal}>Open Modal</Button>
9-
<Modal open={modalOpen} on:close={handleCloseModal}>
10-
<span slot="title">This is the modal demo</span>
11-
<span slot="message">Are you sure you want to print a statement to the console?</span>
12-
<Button
13-
slot="primary"
14-
on:click={() => console.log('statement')}
15-
>
16-
Print
17-
</Button>
18-
<Button slot="secondary" variant="dark" on:click={handleCloseModal}>
19-
Cancel
20-
</Button>
21-
</Modal>
22-
</div>
7+
<script lang="ts">
8+
let isOpen = false
9+
const handleOpen = () => (isOpen = true)
10+
const handleClose = () => (isOpen = false)
11+
</script>
12+
13+
<div>
14+
<Button on:click={handleOpen}>
15+
Open Modal
16+
</Button>
17+
<Modal {isOpen} on:close={handleClose}>
18+
<svelte:fragment slot="title">
19+
This is the modal demo
20+
</svelte:fragment>
21+
<svelte:fragment slot="message">
22+
Are you sure you want to print a statement to the console?
23+
</svelte:fragment>
24+
<Button
25+
slot="primary"
26+
on:click={() => console.log('statement')}
27+
>
28+
Print
29+
</Button>
30+
<Button
31+
slot="secondary"
32+
variant="dark"
33+
on:click={handleClose}
34+
>
35+
Cancel
36+
</Button>
37+
</Modal>
38+
</div>
2339
```
2440
-->
2541
<svelte:options immutable />
2642

2743
<script lang="ts">
2844
import cx from 'classnames';
45+
import { createEventDispatcher } from 'svelte';
2946
import IconButton from './button/icon-button.svelte';
3047
import { clickOutside } from '$lib';
31-
import type { Writable } from 'svelte/store';
3248
3349
/** Whether the modal is open. */
34-
export let isOpen: Writable<boolean>;
50+
export let isOpen: boolean;
3551
36-
$: if (typeof document !== 'undefined') {
37-
document.body.classList.toggle('overflow-hidden', $isOpen);
38-
}
52+
/** The variant of the modal. */
53+
export let variant: 'small' | '' = '';
3954
4055
let headingElement: HTMLElement | undefined;
4156
42-
$: headingElement?.focus();
43-
44-
/** The variant of the modal. */
45-
export let variant: 'small' | '' = '';
57+
const dispatch = createEventDispatcher<{
58+
close: undefined;
59+
}>();
4660
4761
const handleCloseModal = () => {
48-
if ($isOpen) {
49-
isOpen.set(false);
50-
}
62+
dispatch('close');
5163
};
5264
5365
const handleEscapePress = (event: KeyboardEvent) => {
@@ -56,11 +68,17 @@ const handleEscapePress = (event: KeyboardEvent) => {
5668
handleCloseModal();
5769
}
5870
};
71+
72+
$: if (typeof document !== 'undefined') {
73+
document.body.classList.toggle('overflow-hidden', isOpen);
74+
}
75+
76+
$: headingElement?.focus();
5977
</script>
6078

61-
<svelte:window on:keydown={handleEscapePress} />
79+
<svelte:window on:keydown={isOpen ? handleEscapePress : undefined} />
6280

63-
{#if $isOpen}
81+
{#if isOpen}
6482
<div
6583
class="fixed left-0 top-0 z-50 flex h-full w-full items-center justify-center bg-black bg-opacity-40"
6684
role="dialog"

‎packages/core/src/routes/+page.svelte

+40-37
Original file line numberDiff line numberDiff line change
@@ -42,25 +42,25 @@ import {
4242
CodeSnippet,
4343
} from '$lib';
4444
import { uniqueId } from 'lodash';
45-
import { writable } from 'svelte/store';
4645
4746
provideNotify();
4847
4948
let buttonClickedTimes = 0;
5049
let preventHandlerDisabled = true;
51-
const modalOpen = writable(false);
50+
let modalOpen = false;
5251
5352
const handleTogglePreventHandler = (event: CustomEvent<boolean>) => {
5453
preventHandlerDisabled = event.detail;
5554
};
5655
5756
const handleCloseModal = () => {
58-
modalOpen.set(false);
57+
modalOpen = false;
5958
};
6059
6160
const handleOpenModal = () => {
62-
modalOpen.set(true);
61+
modalOpen = true;
6362
};
63+
6464
const notify = useNotify();
6565
6666
let restrictedValue = '';
@@ -114,28 +114,28 @@ const jsSnippet = `
114114
*/
115115
function fizzBuzz(n) {
116116
const result = [];
117-
117+
118118
for (let i = 1; i <= n; i++) {
119119
let output = "";
120-
120+
121121
if (i % 3 === 0) {
122122
output += "Fizz";
123123
}
124-
124+
125125
if (i % 5 === 0) {
126126
output += "Buzz";
127-
}
128-
127+
}
128+
129129
if (output === "") {
130130
output = i.toString();
131131
}
132-
132+
133133
result.push(output);
134134
}
135-
135+
136136
return result;
137137
}
138-
138+
139139
// Usage Example
140140
const fizzBuzzSequence = fizzBuzz(15);
141141
console.log(fizzBuzzSequence);`.trim();
@@ -181,7 +181,7 @@ console.log(sequence); // Outputs: ["1", "2", "Fizz", "4", "Buzz", "Fizz", "7",
181181
182182
const goSnippet = `
183183
import "fmt"
184-
184+
185185
// FizzBuzz
186186
//
187187
// Parameters:
@@ -191,7 +191,7 @@ import "fmt"
191191
// []string: A slice of strings containing the FizzBuzz results for each number from 1 to n.
192192
func FizzBuzz(n int) []string {
193193
result := make([]string, n)
194-
194+
195195
for i := 1; i <= n; i++ {
196196
if i%3 == 0 && i%5 == 0 {
197197
result[i-1] = "FizzBuzz"
@@ -203,16 +203,16 @@ func FizzBuzz(n int) []string {
203203
result[i-1] = fmt.Sprintf("%d", i)
204204
}
205205
}
206-
206+
207207
return result
208208
}
209-
209+
210210
// Usage Example for FizzBuzz
211-
211+
212212
func main() {
213213
// Apply FizzBuzz algorithm up to 20
214214
fizzBuzzResult := FizzBuzz(20)
215-
215+
216216
// Print the FizzBuzz results
217217
for _, value := range fizzBuzzResult {
218218
fmt.Println(value)
@@ -223,27 +223,27 @@ const pythonSnippet = `
223223
def fizzbuzz(n: int):
224224
"""
225225
Function to implement the FizzBuzz algorithm.
226-
226+
227227
Parameters:
228228
- n: int
229229
The number up to which the FizzBuzz algorithm should be applied.
230-
230+
231231
Returns:
232232
- list:
233233
A list of strings representing the FizzBuzz sequence from 1 to n.
234-
234+
235235
Raises:
236236
- ValueError:
237237
Will raise an error if the input number 'n' is less than 1.
238238
"""
239-
239+
240240
# Validating the input number
241241
if n < 1:
242242
raise ValueError("Input number should be greater than or equal to 1.")
243-
243+
244244
# Initializing an empty list to store the FizzBuzz sequence
245245
fizzbuzz_sequence = []
246-
246+
247247
# Looping through numbers from 1 to n (inclusive)
248248
for i in range(1, n+1):
249249
# Checking if the number is divisible by both 3 and 5
@@ -258,9 +258,9 @@ def fizzbuzz(n: int):
258258
# If none of the above conditions are met, add the number itself
259259
else:
260260
fizzbuzz_sequence.append(str(i))
261-
261+
262262
return fizzbuzz_sequence
263-
263+
264264
# Example usage of the fizzbuzz function
265265
n = 20
266266
result = fizzbuzz(n)
@@ -269,41 +269,41 @@ print(result)`.trim();
269269
const cppSnippet = `
270270
#include <iostream>
271271
#include <string>
272-
272+
273273
/**
274274
* @brief Implements the FizzBuzz algorithm.
275-
*
275+
*
276276
* The FizzBuzz algorithm is a common programming task where you iterate over a range of numbers
277277
* and print "Fizz" for numbers divisible by 3, "Buzz" for numbers divisible by 5, and "FizzBuzz"
278278
* for numbers divisible by both 3 and 5. For all other numbers, the number itself is printed.
279-
*
279+
*
280280
* @param n The number of iterations to perform.
281281
*/
282282
void fizzBuzz(int n) {
283283
for (int i = 1; i <= n; i++) {
284284
std::string output = "";
285-
285+
286286
if (i % 3 == 0) {
287287
output += "Fizz";
288288
}
289-
289+
290290
if (i % 5 == 0) {
291291
output += "Buzz";
292292
}
293-
293+
294294
if (output.empty()) {
295295
output = std::to_string(i);
296296
}
297-
297+
298298
std::cout << output << std::endl;
299299
}
300300
}
301-
301+
302302
int main() {
303303
int n = 100; // Number of iterations
304-
304+
305305
fizzBuzz(n);
306-
306+
307307
return 0;
308308
}`.trim();
309309
@@ -1020,7 +1020,10 @@ const htmlSnippet = `
10201020

10211021
<div>
10221022
<Button on:click={handleOpenModal}>Open Modal</Button>
1023-
<Modal isOpen={modalOpen}>
1023+
<Modal
1024+
isOpen={modalOpen}
1025+
on:close={handleCloseModal}
1026+
>
10241027
<span slot="title">This is the modal demo</span>
10251028
<span slot="message"
10261029
>Are you sure you want to kick off a notify toast?</span

‎packages/storybook/.eslintrc.cjs

+3
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,7 @@ module.exports = {
1818
browser: true,
1919
node: true,
2020
},
21+
rules: {
22+
'no-console': 'off',
23+
},
2124
};
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,64 @@
11
<script lang="ts">
22
import { Meta, Story } from '@storybook/addon-svelte-csf';
33
import { Modal, Button } from '@viamrobotics/prime-core';
4-
import { writable } from 'svelte/store';
4+
5+
let defaultModalOpen = false;
6+
let smallModalOpen = false;
57
</script>
68

79
<Meta title="Elements/Modal" />
810

911
<Story name="Default">
10-
<Modal isOpen={writable(true)}>
11-
<svelte:fragment slot="title">Default title</svelte:fragment>
12-
<svelte:fragment slot="message">This is the default modal.</svelte:fragment>
13-
<svelte:fragment slot="primary">
12+
<div class="h-[500px]">
13+
<Button on:click={() => (defaultModalOpen = true)}>Open</Button>
14+
<Modal
15+
isOpen={defaultModalOpen}
16+
on:close={() => (defaultModalOpen = false)}
17+
>
18+
<svelte:fragment slot="title">Default title</svelte:fragment>
19+
<svelte:fragment slot="message"
20+
>This is the default modal.</svelte:fragment
21+
>
1422
<Button
15-
on:click={() => {
16-
/* eslint-disable-next-line no-console */
17-
console.log('Primary button clicked');
18-
}}
23+
slot="primary"
24+
on:click={() => console.log('Primary button clicked')}
1925
>
2026
Primary
2127
</Button>
22-
</svelte:fragment>
23-
<svelte:fragment slot="secondary">
2428
<Button
29+
slot="secondary"
2530
variant="dark"
26-
on:click={() => {
27-
/* eslint-disable-next-line no-console */
28-
console.log('Secondary button clicked');
29-
}}
31+
on:click={() => console.log('Secondary button clicked')}
3032
>
3133
Secondary
3234
</Button>
33-
</svelte:fragment>
34-
</Modal>
35+
</Modal>
36+
</div>
3537
</Story>
3638

3739
<Story name="Small">
38-
<Modal
39-
isOpen={writable(true)}
40-
variant="small"
41-
>
42-
<svelte:fragment slot="title">Small modal title</svelte:fragment>
43-
<svelte:fragment slot="message">This is a small modal.</svelte:fragment>
44-
<svelte:fragment slot="primary">
40+
<div class="h-[200px]">
41+
<Button on:click={() => (smallModalOpen = true)}>Open</Button>
42+
<Modal
43+
isOpen={smallModalOpen}
44+
variant="small"
45+
on:close={() => (smallModalOpen = false)}
46+
>
47+
<svelte:fragment slot="title">Small modal title</svelte:fragment>
48+
<svelte:fragment slot="message">This is a small modal.</svelte:fragment>
4549
<Button
46-
on:click={() => {
47-
/* eslint-disable-next-line no-console */
48-
console.log('Primary button clicked');
49-
}}
50+
slot="primary"
51+
on:click={() => console.log('Primary button clicked')}
5052
>
5153
Primary
5254
</Button>
53-
</svelte:fragment>
54-
<svelte:fragment slot="secondary">
5555
<Button
56+
slot="secondary"
5657
variant="dark"
57-
on:click={() => {
58-
/* eslint-disable-next-line no-console */
59-
console.log('Secondary button clicked');
60-
}}
58+
on:click={() => console.log('Secondary button clicked')}
6159
>
6260
Secondary
6361
</Button>
64-
</svelte:fragment>
65-
</Modal>
62+
</Modal>
63+
</div>
6664
</Story>

0 commit comments

Comments
 (0)
Please sign in to comment.