@instructure/ui-dialog 8.46.2-snapshot-8 → 8.46.2-snapshot-13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,557 @@
1
+ /*
2
+ * The MIT License (MIT)
3
+ *
4
+ * Copyright (c) 2015 - present Instructure, Inc.
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ import React, {
26
+ forwardRef,
27
+ useEffect,
28
+ useImperativeHandle,
29
+ useRef,
30
+ useState
31
+ } from 'react'
32
+ import { fireEvent, render, waitFor } from '@testing-library/react'
33
+ import { userEvent } from '@testing-library/user-event'
34
+ import '@testing-library/jest-dom'
35
+
36
+ import { Dialog } from '../index'
37
+ import type { DialogProps } from '../props'
38
+
39
+ const TEST_TEXT = 'test-text'
40
+ const TEST_LABEL = 'test-label'
41
+
42
+ const renderDialog = (props?: Partial<DialogProps>) => {
43
+ const allProps: DialogProps = {
44
+ open: true,
45
+ label: TEST_LABEL,
46
+ ...props
47
+ }
48
+
49
+ return render(
50
+ <Dialog {...allProps}>
51
+ <button>{TEST_TEXT}</button>
52
+ </Dialog>
53
+ )
54
+ }
55
+
56
+ const DialogExample = forwardRef((props: DialogProps, ref: React.Ref<any>) => {
57
+ const inputRef = useRef<HTMLInputElement | null>(null)
58
+ const dialogRef = useRef<Dialog | null>(null)
59
+
60
+ useEffect(() => {
61
+ if (!props.open) {
62
+ inputRef.current!.focus()
63
+ }
64
+ }, [props.open])
65
+
66
+ useImperativeHandle(ref, () => ({
67
+ focusDialog: () => dialogRef.current!.focus(),
68
+ blurDialog: () => dialogRef.current!.blur()
69
+ }))
70
+
71
+ return (
72
+ <div>
73
+ <input data-testid="input-trigger" type="text" ref={inputRef} />
74
+ <Dialog
75
+ shouldContainFocus
76
+ shouldReturnFocus
77
+ label={TEST_LABEL}
78
+ {...props}
79
+ ref={dialogRef}
80
+ >
81
+ {props.children || (
82
+ <div>
83
+ <input type="text" data-testid="input-one" />
84
+ <input type="text" data-testid="input-two" />
85
+ </div>
86
+ )}
87
+ </Dialog>
88
+ </div>
89
+ )
90
+ })
91
+ DialogExample.displayName = 'DialogExample'
92
+
93
+ const NestedDialogExample = (props: DialogProps) => {
94
+ const [nestedOpen, setNestedOpen] = useState(false)
95
+ const handleTriggerClick = () => setNestedOpen(true)
96
+
97
+ return (
98
+ <div>
99
+ <Dialog open shouldReturnFocus label={TEST_LABEL} {...props}>
100
+ <div>
101
+ <div>
102
+ <input
103
+ onClick={handleTriggerClick}
104
+ type="text"
105
+ data-testid="nested-input-one"
106
+ />
107
+ <input
108
+ onClick={handleTriggerClick}
109
+ type="text"
110
+ data-testid="nested-input-two"
111
+ />
112
+ </div>
113
+ <Dialog open={nestedOpen} label={TEST_LABEL}>
114
+ {TEST_TEXT}
115
+ </Dialog>
116
+ </div>
117
+ </Dialog>
118
+ </div>
119
+ )
120
+ }
121
+ NestedDialogExample.displayName = 'NestedDialogExample'
122
+
123
+ describe('<Dialog />', () => {
124
+ it('should render nothing when closed', () => {
125
+ const { container } = renderDialog({ open: false })
126
+
127
+ expect(container.firstChild).not.toBeInTheDocument()
128
+ })
129
+
130
+ it('should render children when open', async () => {
131
+ const { container } = renderDialog({ open: true })
132
+
133
+ expect(container.firstChild).toBeInTheDocument()
134
+ expect(container.firstChild).toHaveTextContent(TEST_TEXT)
135
+ })
136
+
137
+ it('should apply the a11y attributes', () => {
138
+ const { getByRole, getByLabelText } = renderDialog({ label: TEST_LABEL })
139
+ const dialog = getByRole('dialog')
140
+ const label = getByLabelText(TEST_LABEL)
141
+
142
+ expect(dialog).toBeInTheDocument()
143
+ expect(label).toBeInTheDocument()
144
+ })
145
+
146
+ it('should apply the role attributes, if explicitly passed', () => {
147
+ const { getByRole, getByLabelText } = renderDialog({
148
+ label: TEST_LABEL,
149
+ role: 'region'
150
+ })
151
+ const regionRole = getByRole('region')
152
+ const label = getByLabelText(TEST_LABEL)
153
+
154
+ expect(regionRole).toBeInTheDocument()
155
+ expect(label).toBeInTheDocument()
156
+ })
157
+
158
+ it('should call onDismiss prop when Esc key pressed', async () => {
159
+ const onDismiss = jest.fn()
160
+ const { getByRole } = renderDialog({ onDismiss })
161
+ const dialog = getByRole('dialog')
162
+
163
+ await waitFor(() => {
164
+ fireEvent.keyUp(dialog, {
165
+ key: 'Escape',
166
+ code: 'Escape',
167
+ keyCode: 27,
168
+ charCode: 27
169
+ })
170
+ expect(onDismiss).toHaveBeenCalled()
171
+ })
172
+ })
173
+
174
+ it('should call onDismiss prop when the document is clicked', async () => {
175
+ const onDismiss = jest.fn()
176
+ renderDialog({ onDismiss, shouldCloseOnDocumentClick: true })
177
+
178
+ await waitFor(() => {
179
+ fireEvent.click(document)
180
+ expect(onDismiss).toHaveBeenCalled()
181
+ })
182
+ })
183
+
184
+ describe('managed focus', () => {
185
+ it('should provide focus method', async () => {
186
+ const { getByTestId } = render(
187
+ <div>
188
+ <DialogExample
189
+ open
190
+ contentElement={() => getByTestId('non-tabbable')}
191
+ >
192
+ {TEST_TEXT}
193
+ </DialogExample>
194
+ <div tabIndex={-1} data-testid="non-tabbable">
195
+ {TEST_TEXT}
196
+ </div>
197
+ </div>
198
+ )
199
+ const nonTabbableContent = getByTestId('non-tabbable')
200
+
201
+ await waitFor(() => {
202
+ userEvent.tab()
203
+ expect(document.activeElement).toBe(nonTabbableContent)
204
+ })
205
+ })
206
+
207
+ it('should warn when trying to focus or blur a closed dialog', () => {
208
+ const consoleError = jest
209
+ .spyOn(console, 'error')
210
+ .mockImplementation(() => {})
211
+ let ref
212
+ render(<DialogExample open={false} ref={(el) => (ref = el)} />)
213
+
214
+ ref!.focusDialog()
215
+ expect(consoleError.mock.calls[0][0]).toBe(
216
+ "Warning: [Dialog] Can't focus a Dialog that isn't open."
217
+ )
218
+
219
+ ref!.blurDialog()
220
+ expect(consoleError.mock.calls[1][0]).toBe(
221
+ "Warning: [Dialog] Can't blur a Dialog that isn't open."
222
+ )
223
+
224
+ consoleError.mockRestore()
225
+ })
226
+
227
+ it('should focus the first tabbable element by default', async () => {
228
+ const { getByTestId } = render(<DialogExample open />)
229
+ const inputOne = getByTestId('input-one')
230
+
231
+ await waitFor(() => {
232
+ expect(document.activeElement).toBe(inputOne)
233
+ })
234
+ })
235
+
236
+ it('should focus the first tabbable element when open prop becomes true', async () => {
237
+ const { rerender, getByTestId } = render(<DialogExample open={false} />)
238
+ const inputTrigger = getByTestId('input-trigger')
239
+
240
+ await waitFor(() => {
241
+ expect(document.activeElement).toBe(inputTrigger)
242
+ })
243
+
244
+ rerender(<DialogExample open={true} />)
245
+ const inputOne = getByTestId('input-one')
246
+
247
+ await waitFor(() => {
248
+ expect(document.activeElement).toBe(inputOne)
249
+ })
250
+ })
251
+
252
+ it('should take a prop for finding default focus', async () => {
253
+ const { getByTestId } = render(
254
+ <DialogExample
255
+ open
256
+ defaultFocusElement={() => getByTestId('input-two')}
257
+ />
258
+ )
259
+ const inputTwo = getByTestId('input-two')
260
+
261
+ await waitFor(() => {
262
+ expect(document.activeElement).toBe(inputTwo)
263
+ })
264
+ })
265
+
266
+ it('should still focus the defaultFocusElement when it is focusable but not tabbable', async () => {
267
+ const { getByTestId } = render(
268
+ <DialogExample
269
+ open
270
+ defaultFocusElement={() => getByTestId('non-tabbable')}
271
+ >
272
+ <div tabIndex={-1} data-testid="non-tabbable">
273
+ {TEST_TEXT}
274
+ </div>
275
+ </DialogExample>
276
+ )
277
+ const nonTabbableContent = getByTestId('non-tabbable')
278
+
279
+ await waitFor(() => {
280
+ expect(document.activeElement).toBe(nonTabbableContent)
281
+ })
282
+ })
283
+
284
+ it('should focus the contentElement by default if focusable and no defaultFocusElement is provided', async () => {
285
+ const { getByTestId } = render(
286
+ <div>
287
+ <DialogExample
288
+ open
289
+ contentElement={() => getByTestId('non-tabbable')}
290
+ >
291
+ {TEST_TEXT}
292
+ </DialogExample>
293
+ <div tabIndex={-1} data-testid="non-tabbable">
294
+ {TEST_TEXT}
295
+ </div>
296
+ </div>
297
+ )
298
+ const nonTabbableContent = getByTestId('non-tabbable')
299
+
300
+ await waitFor(() => {
301
+ expect(document.activeElement).toBe(nonTabbableContent)
302
+ })
303
+ })
304
+
305
+ it('should focus the document body if there is no defaultFocusElement, tabbable elements, or focusable contentElement', async () => {
306
+ const { rerender, getByTestId } = render(
307
+ <DialogExample open={false}>{TEST_TEXT}</DialogExample>
308
+ )
309
+ const inputTrigger = getByTestId('input-trigger')
310
+ inputTrigger.focus()
311
+
312
+ rerender(<DialogExample open={true}>{TEST_TEXT}</DialogExample>)
313
+
314
+ await waitFor(() => {
315
+ expect(document.activeElement).toBe(document.body)
316
+ })
317
+ })
318
+
319
+ it('should return focus', async () => {
320
+ const { rerender, getByTestId } = render(<DialogExample open={false} />)
321
+ expect(document.activeElement).toBe(getByTestId('input-trigger'))
322
+
323
+ rerender(<DialogExample open={true} />)
324
+ await waitFor(() => {
325
+ expect(document.activeElement).toBe(getByTestId('input-one'))
326
+ })
327
+
328
+ rerender(<DialogExample open={false} />)
329
+ await waitFor(() => {
330
+ expect(document.activeElement).toBe(getByTestId('input-trigger'))
331
+ })
332
+ })
333
+
334
+ describe('when focus leaves the first and last tabbable', () => {
335
+ it(`should NOT call onBlur when shouldContainFocus=true and tab pressing last tabbable`, async () => {
336
+ const onBlur = jest.fn()
337
+ const { getByTestId } = render(
338
+ <DialogExample
339
+ open
340
+ onBlur={onBlur}
341
+ shouldContainFocus
342
+ defaultFocusElement={() => getByTestId('input-two')}
343
+ />
344
+ )
345
+ const inputOne = getByTestId('input-one')
346
+ const inputTwo = getByTestId('input-two')
347
+
348
+ await waitFor(() => {
349
+ expect(document.activeElement).toBe(inputTwo)
350
+ })
351
+
352
+ await waitFor(() => {
353
+ userEvent.tab()
354
+ expect(onBlur).not.toHaveBeenCalled()
355
+ expect(document.activeElement).toBe(inputOne)
356
+ })
357
+ })
358
+
359
+ it('should NOT call onBlur when shouldContainFocus=true and Shift+Tab pressing first tabbable', async () => {
360
+ const onBlur = jest.fn()
361
+
362
+ const { getByTestId } = render(
363
+ <DialogExample
364
+ open
365
+ shouldContainFocus
366
+ defaultFocusElement={() => getByTestId('input-one')}
367
+ onBlur={onBlur}
368
+ />
369
+ )
370
+ const inputOne = getByTestId('input-one')
371
+ const inputTwo = getByTestId('input-two')
372
+
373
+ await waitFor(() => {
374
+ expect(document.activeElement).toBe(inputOne)
375
+
376
+ fireEvent.keyDown(inputOne, {
377
+ key: 'Tab',
378
+ code: 'Tab',
379
+ keyCode: 9,
380
+ charCode: 9,
381
+ shiftKey: true
382
+ })
383
+
384
+ expect(onBlur).not.toHaveBeenCalled()
385
+ expect(document.activeElement).toBe(inputTwo)
386
+ })
387
+ })
388
+
389
+ it('should call onBlur when shouldContainFocus=false and tab pressing last tabbable', async () => {
390
+ const onBlur = jest.fn()
391
+
392
+ const { getByTestId } = render(
393
+ <DialogExample
394
+ open
395
+ shouldContainFocus={false}
396
+ defaultFocusElement={() => getByTestId('input-two')}
397
+ onBlur={onBlur}
398
+ />
399
+ )
400
+ const inputTwo = getByTestId('input-two')
401
+ inputTwo.focus()
402
+
403
+ await waitFor(() => {
404
+ expect(document.activeElement).toBe(inputTwo)
405
+
406
+ fireEvent.keyDown(inputTwo, {
407
+ key: 'Tab',
408
+ code: 'Tab',
409
+ keyCode: 9,
410
+ charCode: 9
411
+ })
412
+ expect(onBlur).toHaveBeenCalled()
413
+ })
414
+ })
415
+
416
+ it('should call onBlur when shouldContainFocus=false and pressing Shift+Tab on the first tabbable', async () => {
417
+ const onBlur = jest.fn()
418
+
419
+ const { getByTestId } = render(
420
+ <DialogExample
421
+ open
422
+ shouldContainFocus={false}
423
+ defaultFocusElement={() => getByTestId('input-one')}
424
+ onBlur={onBlur}
425
+ />
426
+ )
427
+ const inputOne = getByTestId('input-one')
428
+ inputOne.focus()
429
+
430
+ await waitFor(() => {
431
+ expect(document.activeElement).toBe(inputOne)
432
+
433
+ fireEvent.keyDown(inputOne, {
434
+ key: 'Tab',
435
+ code: 'Tab',
436
+ keyCode: 9,
437
+ charCode: 9,
438
+ shiftKey: true
439
+ })
440
+ expect(onBlur).toHaveBeenCalled()
441
+ })
442
+ })
443
+
444
+ describe('when launching a dialog w/out focusable content from another dialog', () => {
445
+ it(`should contain focus when last tabbable element triggers dialog w/out focusable content`, async () => {
446
+ const onBlur = jest.fn()
447
+
448
+ const { getByTestId } = render(
449
+ <NestedDialogExample
450
+ onBlur={onBlur}
451
+ shouldContainFocus
452
+ defaultFocusElement={() => getByTestId('nested-input-two')}
453
+ />
454
+ )
455
+ const inputOne = getByTestId('nested-input-one')
456
+ const inputTwo = getByTestId('nested-input-two')
457
+
458
+ await waitFor(() => {
459
+ userEvent.click(inputTwo)
460
+ expect(document.activeElement).toBe(inputTwo)
461
+
462
+ fireEvent.keyDown(inputTwo, {
463
+ key: 'Tab',
464
+ code: 'Tab',
465
+ keyCode: 9,
466
+ charCode: 9
467
+ })
468
+ expect(onBlur).not.toHaveBeenCalled()
469
+ expect(document.activeElement).toBe(inputOne)
470
+ })
471
+ })
472
+
473
+ it('should contain focus when first tabbable element triggers dialog w/out focusable content', async () => {
474
+ const onBlur = jest.fn()
475
+
476
+ const { getByTestId } = render(
477
+ <NestedDialogExample
478
+ onBlur={onBlur}
479
+ shouldContainFocus
480
+ defaultFocusElement={() => getByTestId('nested-input-one')}
481
+ />
482
+ )
483
+ const inputOne = getByTestId('nested-input-one')
484
+ const inputTwo = getByTestId('nested-input-two')
485
+
486
+ await waitFor(() => {
487
+ userEvent.click(inputOne)
488
+ expect(document.activeElement).toBe(inputOne)
489
+
490
+ fireEvent.keyDown(inputOne, {
491
+ key: 'Tab',
492
+ code: 'Tab',
493
+ keyCode: 9,
494
+ charCode: 9,
495
+ shiftKey: true
496
+ })
497
+ expect(onBlur).not.toHaveBeenCalled()
498
+ expect(document.activeElement).toBe(inputTwo)
499
+ })
500
+ })
501
+
502
+ it(`should call onBlur when shouldContainFocus=false and last tabbable element triggers dialog w/out focusable content`, async () => {
503
+ const onBlur = jest.fn()
504
+
505
+ const { getByTestId } = render(
506
+ <NestedDialogExample
507
+ onBlur={onBlur}
508
+ shouldContainFocus={false}
509
+ defaultFocusElement={() => getByTestId('nested-input-two')}
510
+ />
511
+ )
512
+ const inputTwo = getByTestId('nested-input-two')
513
+
514
+ await waitFor(() => {
515
+ userEvent.click(inputTwo)
516
+ expect(document.activeElement).toBe(inputTwo)
517
+
518
+ fireEvent.keyDown(inputTwo, {
519
+ key: 'Tab',
520
+ code: 'Tab',
521
+ keyCode: 9,
522
+ charCode: 9
523
+ })
524
+ expect(onBlur).toHaveBeenCalled()
525
+ })
526
+ })
527
+
528
+ it(`should call onBlur when shouldContainFocus=false and first tabbable element triggers dialog w/out focusable content`, async () => {
529
+ const onBlur = jest.fn()
530
+
531
+ const { getByTestId } = render(
532
+ <NestedDialogExample
533
+ onBlur={onBlur}
534
+ shouldContainFocus={false}
535
+ defaultFocusElement={() => getByTestId('nested-input-one')}
536
+ />
537
+ )
538
+ const inputOne = getByTestId('nested-input-one')
539
+
540
+ await waitFor(() => {
541
+ userEvent.click(inputOne)
542
+ expect(document.activeElement).toBe(inputOne)
543
+
544
+ fireEvent.keyDown(inputOne, {
545
+ key: 'Tab',
546
+ code: 'Tab',
547
+ keyCode: 9,
548
+ charCode: 9,
549
+ shiftKey: true
550
+ })
551
+ expect(onBlur).toHaveBeenCalled()
552
+ })
553
+ })
554
+ })
555
+ })
556
+ })
557
+ })
@@ -38,7 +38,6 @@ import type { DialogProps } from './props'
38
38
  category: components/utilities
39
39
  ---
40
40
  @module Dialog
41
- @tsProps
42
41
  **/
43
42
  class Dialog extends Component<DialogProps> {
44
43
  static readonly componentId = 'Dialog'
@@ -14,7 +14,6 @@
14
14
  { "path": "../ui-testable/tsconfig.build.json" },
15
15
  { "path": "../console/tsconfig.build.json" },
16
16
  { "path": "../ui-babel-preset/tsconfig.build.json" },
17
- { "path": "../ui-prop-types/tsconfig.build.json" },
18
- { "path": "../ui-test-utils/tsconfig.build.json" }
17
+ { "path": "../ui-prop-types/tsconfig.build.json" }
19
18
  ]
20
19
  }