• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

twbs / bootstrap / 9856469587

09 Jul 2024 11:49AM CUT coverage: 96.07%. Remained the same
9856469587

Pull #40619

github

web-flow
Merge ac79a1794 into 7c392498f
Pull Request #40619: Docs: Fix a minor accessibility issue (checkout example missing h1)

666 of 726 branches covered (91.74%)

Branch coverage included in aggregate %.

2023 of 2073 relevant lines covered (97.59%)

249.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.75
/js/src/modal.js
1
/**
2
 * --------------------------------------------------------------------------
3
 * Bootstrap modal.js
4
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5
 * --------------------------------------------------------------------------
6
 */
7

8
import BaseComponent from './base-component.js'
9
import EventHandler from './dom/event-handler.js'
10
import SelectorEngine from './dom/selector-engine.js'
11
import Backdrop from './util/backdrop.js'
12
import { enableDismissTrigger } from './util/component-functions.js'
13
import FocusTrap from './util/focustrap.js'
14
import {
15
  defineJQueryPlugin, isRTL, isVisible, reflow
16
} from './util/index.js'
17
import ScrollBarHelper from './util/scrollbar.js'
18

19
/**
20
 * Constants
21
 */
22

23
const NAME = 'modal'
1✔
24
const DATA_KEY = 'bs.modal'
1✔
25
const EVENT_KEY = `.${DATA_KEY}`
1✔
26
const DATA_API_KEY = '.data-api'
1✔
27
const ESCAPE_KEY = 'Escape'
1✔
28

29
const EVENT_HIDE = `hide${EVENT_KEY}`
1✔
30
const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`
1✔
31
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
1✔
32
const EVENT_SHOW = `show${EVENT_KEY}`
1✔
33
const EVENT_SHOWN = `shown${EVENT_KEY}`
1✔
34
const EVENT_RESIZE = `resize${EVENT_KEY}`
1✔
35
const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`
1✔
36
const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`
1✔
37
const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`
1✔
38
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
1✔
39

40
const CLASS_NAME_OPEN = 'modal-open'
1✔
41
const CLASS_NAME_FADE = 'fade'
1✔
42
const CLASS_NAME_SHOW = 'show'
1✔
43
const CLASS_NAME_STATIC = 'modal-static'
1✔
44

45
const OPEN_SELECTOR = '.modal.show'
1✔
46
const SELECTOR_DIALOG = '.modal-dialog'
1✔
47
const SELECTOR_MODAL_BODY = '.modal-body'
1✔
48
const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="modal"]'
1✔
49

50
const Default = {
1✔
51
  backdrop: true,
52
  focus: true,
53
  keyboard: true
54
}
55

56
const DefaultType = {
1✔
57
  backdrop: '(boolean|string)',
58
  focus: 'boolean',
59
  keyboard: 'boolean'
60
}
61

62
/**
63
 * Class definition
64
 */
65

66
class Modal extends BaseComponent {
67
  constructor(element, config) {
68
    super(element, config)
58✔
69

70
    this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)
58✔
71
    this._backdrop = this._initializeBackDrop()
58✔
72
    this._focustrap = this._initializeFocusTrap()
58✔
73
    this._isShown = false
58✔
74
    this._isTransitioning = false
58✔
75
    this._scrollBar = new ScrollBarHelper()
58✔
76

77
    this._addEventListeners()
58✔
78
  }
79

80
  // Getters
81
  static get Default() {
82
    return Default
59✔
83
  }
84

85
  static get DefaultType() {
86
    return DefaultType
58✔
87
  }
88

89
  static get NAME() {
90
    return NAME
105✔
91
  }
92

93
  // Public
94
  toggle(relatedTarget) {
95
    return this._isShown ? this.hide() : this.show(relatedTarget)
11✔
96
  }
97

98
  show(relatedTarget) {
99
    if (this._isShown || this._isTransitioning) {
43✔
100
      return
2✔
101
    }
102

103
    const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {
41✔
104
      relatedTarget
105
    })
106

107
    if (showEvent.defaultPrevented) {
41✔
108
      return
3✔
109
    }
110

111
    this._isShown = true
38✔
112
    this._isTransitioning = true
38✔
113

114
    this._scrollBar.hide()
38✔
115

116
    document.body.classList.add(CLASS_NAME_OPEN)
38✔
117

118
    this._adjustDialog()
38✔
119

120
    this._backdrop.show(() => this._showElement(relatedTarget))
38✔
121
  }
122

123
  hide() {
124
    if (!this._isShown || this._isTransitioning) {
16✔
125
      return
2✔
126
    }
127

128
    const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)
14✔
129

130
    if (hideEvent.defaultPrevented) {
14✔
131
      return
1✔
132
    }
133

134
    this._isShown = false
13✔
135
    this._isTransitioning = true
13✔
136
    this._focustrap.deactivate()
13✔
137

138
    this._element.classList.remove(CLASS_NAME_SHOW)
13✔
139

140
    this._queueCallback(() => this._hideModal(), this._element, this._isAnimated())
13✔
141
  }
142

143
  dispose() {
144
    EventHandler.off(window, EVENT_KEY)
1✔
145
    EventHandler.off(this._dialog, EVENT_KEY)
1✔
146

147
    this._backdrop.dispose()
1✔
148
    this._focustrap.deactivate()
1✔
149

150
    super.dispose()
1✔
151
  }
152

153
  handleUpdate() {
154
    this._adjustDialog()
1✔
155
  }
156

157
  // Private
158
  _initializeBackDrop() {
159
    return new Backdrop({
58✔
160
      isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value,
161
      isAnimated: this._isAnimated()
162
    })
163
  }
164

165
  _initializeFocusTrap() {
166
    return new FocusTrap({
58✔
167
      trapElement: this._element
168
    })
169
  }
170

171
  _showElement(relatedTarget) {
172
    // try to append dynamic modal
173
    if (!document.body.contains(this._element)) {
38✔
174
      document.body.append(this._element)
1✔
175
    }
176

177
    this._element.style.display = 'block'
38✔
178
    this._element.removeAttribute('aria-hidden')
38✔
179
    this._element.setAttribute('aria-modal', true)
38✔
180
    this._element.setAttribute('role', 'dialog')
38✔
181
    this._element.scrollTop = 0
38✔
182

183
    const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog)
38✔
184
    if (modalBody) {
38✔
185
      modalBody.scrollTop = 0
2✔
186
    }
187

188
    reflow(this._element)
38✔
189

190
    this._element.classList.add(CLASS_NAME_SHOW)
38✔
191

192
    const transitionComplete = () => {
38✔
193
      if (this._config.focus) {
38✔
194
        this._focustrap.activate()
37✔
195
      }
196

197
      this._isTransitioning = false
38✔
198
      EventHandler.trigger(this._element, EVENT_SHOWN, {
38✔
199
        relatedTarget
200
      })
201
    }
202

203
    this._queueCallback(transitionComplete, this._dialog, this._isAnimated())
38✔
204
  }
205

206
  _addEventListeners() {
207
    EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {
58✔
208
      if (event.key !== ESCAPE_KEY) {
4✔
209
        return
1✔
210
      }
211

212
      if (this._config.keyboard) {
3✔
213
        this.hide()
2✔
214
        return
2✔
215
      }
216

217
      this._triggerBackdropTransition()
1✔
218
    })
219

220
    EventHandler.on(window, EVENT_RESIZE, () => {
58✔
221
      if (this._isShown && !this._isTransitioning) {
100✔
222
        this._adjustDialog()
41✔
223
      }
224
    })
225

226
    EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {
58✔
227
      // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks
228
      EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {
4✔
229
        if (this._element !== event.target || this._element !== event2.target) {
4✔
230
          return
1✔
231
        }
232

233
        if (this._config.backdrop === 'static') {
3✔
234
          this._triggerBackdropTransition()
2✔
235
          return
2✔
236
        }
237

238
        if (this._config.backdrop) {
1✔
239
          this.hide()
1✔
240
        }
241
      })
242
    })
243
  }
244

245
  _hideModal() {
246
    this._element.style.display = 'none'
13✔
247
    this._element.setAttribute('aria-hidden', true)
13✔
248
    this._element.removeAttribute('aria-modal')
13✔
249
    this._element.removeAttribute('role')
13✔
250
    this._isTransitioning = false
13✔
251

252
    this._backdrop.hide(() => {
13✔
253
      document.body.classList.remove(CLASS_NAME_OPEN)
13✔
254
      this._resetAdjustments()
13✔
255
      this._scrollBar.reset()
13✔
256
      EventHandler.trigger(this._element, EVENT_HIDDEN)
13✔
257
    })
258
  }
259

260
  _isAnimated() {
261
    return this._element.classList.contains(CLASS_NAME_FADE)
110✔
262
  }
263

264
  _triggerBackdropTransition() {
265
    const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)
3✔
266
    if (hideEvent.defaultPrevented) {
3!
267
      return
×
268
    }
269

270
    const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
3✔
271
    const initialOverflowY = this._element.style.overflowY
3✔
272
    // return if the following background transition hasn't yet completed
273
    if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {
3✔
274
      return
1✔
275
    }
276

277
    if (!isModalOverflowing) {
2✔
278
      this._element.style.overflowY = 'hidden'
2✔
279
    }
280

281
    this._element.classList.add(CLASS_NAME_STATIC)
2✔
282
    this._queueCallback(() => {
2✔
283
      this._element.classList.remove(CLASS_NAME_STATIC)
2✔
284
      this._queueCallback(() => {
2✔
285
        this._element.style.overflowY = initialOverflowY
2✔
286
      }, this._dialog)
287
    }, this._dialog)
288

289
    this._element.focus()
2✔
290
  }
291

292
  /**
293
   * The following methods are used to handle overflowing modals
294
   */
295

296
  _adjustDialog() {
297
    const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
79✔
298
    const scrollbarWidth = this._scrollBar.getWidth()
79✔
299
    const isBodyOverflowing = scrollbarWidth > 0
79✔
300

301
    if (isBodyOverflowing && !isModalOverflowing) {
79!
302
      const property = isRTL() ? 'paddingLeft' : 'paddingRight'
×
303
      this._element.style[property] = `${scrollbarWidth}px`
×
304
    }
305

306
    if (!isBodyOverflowing && isModalOverflowing) {
79!
307
      const property = isRTL() ? 'paddingRight' : 'paddingLeft'
×
308
      this._element.style[property] = `${scrollbarWidth}px`
×
309
    }
310
  }
311

312
  _resetAdjustments() {
313
    this._element.style.paddingLeft = ''
13✔
314
    this._element.style.paddingRight = ''
13✔
315
  }
316

317
  // Static
318
  static jQueryInterface(config, relatedTarget) {
319
    return this.each(function () {
6✔
320
      const data = Modal.getOrCreateInstance(this, config)
6✔
321

322
      if (typeof config !== 'string') {
6✔
323
        return
4✔
324
      }
325

326
      if (typeof data[config] === 'undefined') {
2✔
327
        throw new TypeError(`No method named "${config}"`)
1✔
328
      }
329

330
      data[config](relatedTarget)
1✔
331
    })
332
  }
333
}
334

335
/**
336
 * Data API implementation
337
 */
338

339
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
1✔
340
  const target = SelectorEngine.getElementFromSelector(this)
9✔
341

342
  if (['A', 'AREA'].includes(this.tagName)) {
9✔
343
    event.preventDefault()
4✔
344
  }
345

346
  EventHandler.one(target, EVENT_SHOW, showEvent => {
9✔
347
    if (showEvent.defaultPrevented) {
9✔
348
      // only register focus restorer if modal will actually get shown
349
      return
1✔
350
    }
351

352
    EventHandler.one(target, EVENT_HIDDEN, () => {
8✔
353
      if (isVisible(this)) {
3✔
354
        this.focus()
2✔
355
      }
356
    })
357
  })
358

359
  // avoid conflict when clicking modal toggler while another one is open
360
  const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)
9✔
361
  if (alreadyOpen) {
9✔
362
    Modal.getInstance(alreadyOpen).hide()
2✔
363
  }
364

365
  const data = Modal.getOrCreateInstance(target)
9✔
366

367
  data.toggle(this)
9✔
368
})
369

370
enableDismissTrigger(Modal)
1✔
371

372
/**
373
 * jQuery
374
 */
375

376
defineJQueryPlugin(Modal)
1✔
377

378
export default Modal
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc