@rhavenside/baseline 2.0.0

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.
Files changed (80) hide show
  1. package/README.md +472 -0
  2. package/dist/base-line.css +5 -0
  3. package/dist/base-line.css.map +1 -0
  4. package/dist/fonts/GoogleSansCode-Bold.ttf +0 -0
  5. package/dist/fonts/GoogleSansCode-BoldItalic.ttf +0 -0
  6. package/dist/fonts/GoogleSansCode-ExtraBold.ttf +0 -0
  7. package/dist/fonts/GoogleSansCode-ExtraBoldItalic.ttf +0 -0
  8. package/dist/fonts/GoogleSansCode-Italic-VariableFont_wght.ttf +0 -0
  9. package/dist/fonts/GoogleSansCode-Italic.ttf +0 -0
  10. package/dist/fonts/GoogleSansCode-Light.ttf +0 -0
  11. package/dist/fonts/GoogleSansCode-LightItalic.ttf +0 -0
  12. package/dist/fonts/GoogleSansCode-Medium.ttf +0 -0
  13. package/dist/fonts/GoogleSansCode-MediumItalic.ttf +0 -0
  14. package/dist/fonts/GoogleSansCode-Regular.ttf +0 -0
  15. package/dist/fonts/GoogleSansCode-SemiBold.ttf +0 -0
  16. package/dist/fonts/GoogleSansCode-SemiBoldItalic.ttf +0 -0
  17. package/dist/fonts/GoogleSansCode-VariableFont_wght.ttf +0 -0
  18. package/dist/fonts/RobotoCondensed-Black.ttf +0 -0
  19. package/dist/fonts/RobotoCondensed-BlackItalic.ttf +0 -0
  20. package/dist/fonts/RobotoCondensed-Bold.ttf +0 -0
  21. package/dist/fonts/RobotoCondensed-BoldItalic.ttf +0 -0
  22. package/dist/fonts/RobotoCondensed-ExtraBold.ttf +0 -0
  23. package/dist/fonts/RobotoCondensed-ExtraBoldItalic.ttf +0 -0
  24. package/dist/fonts/RobotoCondensed-ExtraLight.ttf +0 -0
  25. package/dist/fonts/RobotoCondensed-ExtraLightItalic.ttf +0 -0
  26. package/dist/fonts/RobotoCondensed-Italic-VariableFont_wght.ttf +0 -0
  27. package/dist/fonts/RobotoCondensed-Italic.ttf +0 -0
  28. package/dist/fonts/RobotoCondensed-Light.ttf +0 -0
  29. package/dist/fonts/RobotoCondensed-LightItalic.ttf +0 -0
  30. package/dist/fonts/RobotoCondensed-Medium.ttf +0 -0
  31. package/dist/fonts/RobotoCondensed-MediumItalic.ttf +0 -0
  32. package/dist/fonts/RobotoCondensed-Regular.ttf +0 -0
  33. package/dist/fonts/RobotoCondensed-SemiBold.ttf +0 -0
  34. package/dist/fonts/RobotoCondensed-SemiBoldItalic.ttf +0 -0
  35. package/dist/fonts/RobotoCondensed-Thin.ttf +0 -0
  36. package/dist/fonts/RobotoCondensed-ThinItalic.ttf +0 -0
  37. package/dist/fonts/RobotoCondensed-VariableFont_wght.ttf +0 -0
  38. package/dist/fonts/ZalandoSansExpanded-Black.ttf +0 -0
  39. package/dist/fonts/ZalandoSansExpanded-BlackItalic.ttf +0 -0
  40. package/dist/fonts/ZalandoSansExpanded-Bold.ttf +0 -0
  41. package/dist/fonts/ZalandoSansExpanded-BoldItalic.ttf +0 -0
  42. package/dist/fonts/ZalandoSansExpanded-ExtraBold.ttf +0 -0
  43. package/dist/fonts/ZalandoSansExpanded-ExtraBoldItalic.ttf +0 -0
  44. package/dist/fonts/ZalandoSansExpanded-ExtraLight.ttf +0 -0
  45. package/dist/fonts/ZalandoSansExpanded-ExtraLightItalic.ttf +0 -0
  46. package/dist/fonts/ZalandoSansExpanded-Italic-VariableFont_wght.ttf +0 -0
  47. package/dist/fonts/ZalandoSansExpanded-Italic.ttf +0 -0
  48. package/dist/fonts/ZalandoSansExpanded-Light.ttf +0 -0
  49. package/dist/fonts/ZalandoSansExpanded-LightItalic.ttf +0 -0
  50. package/dist/fonts/ZalandoSansExpanded-Medium.ttf +0 -0
  51. package/dist/fonts/ZalandoSansExpanded-MediumItalic.ttf +0 -0
  52. package/dist/fonts/ZalandoSansExpanded-Regular.ttf +0 -0
  53. package/dist/fonts/ZalandoSansExpanded-SemiBold.ttf +0 -0
  54. package/dist/fonts/ZalandoSansExpanded-SemiBoldItalic.ttf +0 -0
  55. package/dist/fonts/ZalandoSansExpanded-VariableFont_wght.ttf +0 -0
  56. package/dist/fonts/baseline-icons.woff +0 -0
  57. package/dist/fonts/baseline-icons.woff2 +0 -0
  58. package/dist/js/accordion.js +103 -0
  59. package/dist/js/alert.js +91 -0
  60. package/dist/js/base.js +146 -0
  61. package/dist/js/button.js +80 -0
  62. package/dist/js/carousel.js +427 -0
  63. package/dist/js/collapse.js +233 -0
  64. package/dist/js/color-modes.js +70 -0
  65. package/dist/js/component.js +114 -0
  66. package/dist/js/dropdown.js +348 -0
  67. package/dist/js/index.js +108 -0
  68. package/dist/js/modal.js +440 -0
  69. package/dist/js/offcanvas.js +356 -0
  70. package/dist/js/popover.js +241 -0
  71. package/dist/js/swipe.js +143 -0
  72. package/dist/js/tab.js +285 -0
  73. package/dist/js/toast.js +228 -0
  74. package/dist/js/tooltip.js +716 -0
  75. package/dist/js/util/backdrop.js +133 -0
  76. package/dist/js/util/component-functions.js +111 -0
  77. package/dist/js/util/focustrap.js +101 -0
  78. package/dist/js/util/scrollbar.js +111 -0
  79. package/dist/js/util.js +564 -0
  80. package/package.json +47 -0
@@ -0,0 +1,440 @@
1
+ /**
2
+ * Base-Line Modal Component
3
+ * Baseline 2.0 compatible modal with Base-Line naming
4
+ */
5
+
6
+ import { BaseComponent } from './base.js'
7
+ import { SelectorEngine, isRTL, isVisible, reflow, getElement, one } from './util.js'
8
+ import { on, off } from './util.js'
9
+ import Backdrop from './util/backdrop.js'
10
+ import { enableDismissTrigger } from './util/component-functions.js'
11
+ import FocusTrap from './util/focustrap.js'
12
+ import ScrollBarHelper from './util/scrollbar.js'
13
+
14
+ /**
15
+ * Constants
16
+ */
17
+
18
+ const NAME = 'modal'
19
+ const DATA_KEY = `c.${NAME}`
20
+ const EVENT_KEY = `.${DATA_KEY}`
21
+ const DATA_API_KEY = `[data-c-${NAME}]`
22
+ const ESCAPE_KEY = 'Escape'
23
+
24
+ const EVENT_HIDE = `hide${EVENT_KEY}`
25
+ const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`
26
+ const EVENT_HIDDEN = `hidden${EVENT_KEY}`
27
+ const EVENT_SHOW = `show${EVENT_KEY}`
28
+ const EVENT_SHOWN = `shown${EVENT_KEY}`
29
+ const EVENT_RESIZE = `resize${EVENT_KEY}`
30
+ const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`
31
+ const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`
32
+ const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`
33
+ const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
34
+
35
+ const CLASS_NAME_OPEN = 'c-modal-open'
36
+ const CLASS_NAME_FADE = 'c-fade'
37
+ const CLASS_NAME_SHOW = 'is-show'
38
+ const CLASS_NAME_STATIC = 'c-modal-static'
39
+
40
+ const OPEN_SELECTOR = '.c-modal.is-show'
41
+ const SELECTOR_DIALOG = '.c-modal-dialog'
42
+ const SELECTOR_MODAL_BODY = '.c-modal-body'
43
+ const SELECTOR_DATA_TOGGLE = '[data-c-toggle="modal"]'
44
+
45
+ const Default = {
46
+ backdrop: true,
47
+ focus: true,
48
+ keyboard: true
49
+ }
50
+
51
+ /**
52
+ * Class definition
53
+ */
54
+
55
+ class Modal extends BaseComponent {
56
+ constructor(element, config) {
57
+ super(element, config)
58
+
59
+ this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)
60
+ this._backdrop = this._initializeBackDrop()
61
+ this._focustrap = this._initializeFocusTrap()
62
+ this._isShown = false
63
+ this._isTransitioning = false
64
+ this._scrollBar = new ScrollBarHelper()
65
+
66
+ this._addEventListeners()
67
+ }
68
+
69
+ // Getters
70
+ static get Default() {
71
+ return Default
72
+ }
73
+
74
+ static get DefaultType() {
75
+ return {
76
+ backdrop: '(boolean|string)',
77
+ focus: 'boolean',
78
+ keyboard: 'boolean'
79
+ }
80
+ }
81
+
82
+ static get NAME() {
83
+ return NAME
84
+ }
85
+
86
+ // Public
87
+ toggle(relatedTarget) {
88
+ return this._isShown ? this.hide() : this.show(relatedTarget)
89
+ }
90
+
91
+ show(relatedTarget) {
92
+ if (this._isShown || this._isTransitioning) {
93
+ return
94
+ }
95
+
96
+ const showEvent = new CustomEvent(EVENT_SHOW, {
97
+ bubbles: true,
98
+ cancelable: true,
99
+ detail: { relatedTarget }
100
+ })
101
+
102
+ this._element.dispatchEvent(showEvent)
103
+
104
+ if (showEvent.defaultPrevented) {
105
+ return
106
+ }
107
+
108
+ this._isShown = true
109
+ this._isTransitioning = true
110
+
111
+ this._scrollBar.hide()
112
+
113
+ document.body.classList.add(CLASS_NAME_OPEN)
114
+
115
+ this._adjustDialog()
116
+
117
+ this._backdrop.show(() => this._showElement(relatedTarget))
118
+ }
119
+
120
+ hide() {
121
+ if (!this._isShown || this._isTransitioning) {
122
+ return
123
+ }
124
+
125
+ const hideEvent = new CustomEvent(EVENT_HIDE, {
126
+ bubbles: true,
127
+ cancelable: true
128
+ })
129
+
130
+ this._element.dispatchEvent(hideEvent)
131
+
132
+ if (hideEvent.defaultPrevented) {
133
+ return
134
+ }
135
+
136
+ this._isShown = false
137
+ this._isTransitioning = true
138
+
139
+ // Deactivate focus trap and remove focus from modal element BEFORE removing class
140
+ this._focustrap.deactivate()
141
+ // Remove focus from modal element itself if it has focus
142
+ if (this._element === document.activeElement || this._element.contains(document.activeElement)) {
143
+ document.activeElement?.blur()
144
+ }
145
+
146
+ this._element.classList.remove(CLASS_NAME_SHOW)
147
+
148
+ this._queueCallback(() => this._hideModal(), this._element, this._isAnimated())
149
+ }
150
+
151
+ dispose() {
152
+ off(window, EVENT_KEY)
153
+ off(this._dialog, EVENT_KEY)
154
+
155
+ this._backdrop.dispose()
156
+ this._focustrap.deactivate()
157
+
158
+ super.dispose()
159
+ }
160
+
161
+ handleUpdate() {
162
+ this._adjustDialog()
163
+ }
164
+
165
+ // Private
166
+ _initializeBackDrop() {
167
+ const clickCallback = () => {
168
+ if (this._config.backdrop === 'static') {
169
+ this._triggerBackdropTransition()
170
+ return
171
+ }
172
+
173
+ if (this._config.backdrop) {
174
+ this.hide()
175
+ }
176
+ }
177
+
178
+ return new Backdrop({
179
+ isVisible: Boolean(this._config.backdrop),
180
+ isAnimated: this._isAnimated(),
181
+ clickCallback
182
+ })
183
+ }
184
+
185
+ _initializeFocusTrap() {
186
+ return new FocusTrap({
187
+ trapElement: this._element
188
+ })
189
+ }
190
+
191
+ _showElement(relatedTarget) {
192
+ // try to append dynamic modal
193
+ if (!document.body.contains(this._element)) {
194
+ document.body.append(this._element)
195
+ }
196
+
197
+ // Remove aria-hidden BEFORE setting display and focus
198
+ this._element.removeAttribute('aria-hidden')
199
+
200
+ this._element.style.display = 'block'
201
+ this._element.setAttribute('aria-modal', true)
202
+ this._element.setAttribute('role', 'dialog')
203
+ this._element.scrollTop = 0
204
+
205
+ const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog)
206
+ if (modalBody) {
207
+ modalBody.scrollTop = 0
208
+ }
209
+
210
+ reflow(this._element)
211
+
212
+ this._element.classList.add(CLASS_NAME_SHOW)
213
+
214
+ const transitionComplete = () => {
215
+ if (this._config.focus) {
216
+ this._focustrap.activate()
217
+ }
218
+
219
+ this._isTransitioning = false
220
+ const shownEvent = new CustomEvent(EVENT_SHOWN, {
221
+ bubbles: true,
222
+ detail: { relatedTarget }
223
+ })
224
+ this._element.dispatchEvent(shownEvent)
225
+ }
226
+
227
+ this._queueCallback(transitionComplete, this._dialog, this._isAnimated())
228
+ }
229
+
230
+ _addEventListeners() {
231
+ on(this._element, EVENT_KEYDOWN_DISMISS, (event) => {
232
+ if (event.key !== ESCAPE_KEY) {
233
+ return
234
+ }
235
+
236
+ if (this._config.keyboard) {
237
+ this.hide()
238
+ return
239
+ }
240
+
241
+ this._triggerBackdropTransition()
242
+ })
243
+
244
+ on(window, EVENT_RESIZE, () => {
245
+ if (this._isShown && !this._isTransitioning) {
246
+ this._adjustDialog()
247
+ }
248
+ })
249
+
250
+ on(this._element, EVENT_MOUSEDOWN_DISMISS, (event) => {
251
+ // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks
252
+ const clickHandler = (event2) => {
253
+ if (this._element !== event.target || this._element !== event2.target) {
254
+ return
255
+ }
256
+
257
+ if (this._config.backdrop === 'static') {
258
+ this._triggerBackdropTransition()
259
+ return
260
+ }
261
+
262
+ if (this._config.backdrop) {
263
+ this.hide()
264
+ }
265
+ }
266
+
267
+ one(this._element, EVENT_CLICK_DISMISS, clickHandler)
268
+ })
269
+ }
270
+
271
+ _hideModal() {
272
+ // Remove focus from any element inside the modal before hiding
273
+ const activeElement = document.activeElement
274
+ if (this._element.contains(activeElement)) {
275
+ activeElement.blur()
276
+ }
277
+
278
+ // Remove focus from modal element itself if it has focus
279
+ if (this._element === document.activeElement) {
280
+ this._element.blur()
281
+ }
282
+
283
+ this._element.style.display = 'none'
284
+ // Remove aria-modal and role first
285
+ this._element.removeAttribute('aria-modal')
286
+ this._element.removeAttribute('role')
287
+ this._isTransitioning = false
288
+
289
+ this._backdrop.hide(() => {
290
+ document.body.classList.remove(CLASS_NAME_OPEN)
291
+ this._resetAdjustments()
292
+ this._scrollBar.reset()
293
+
294
+ // Ensure no element inside modal has focus before setting aria-hidden
295
+ const focusedElement = document.activeElement
296
+ if (this._element.contains(focusedElement) || this._element === focusedElement) {
297
+ // If focus is still inside modal, move it to body
298
+ document.body.focus()
299
+ // If body can't receive focus, blur the element
300
+ if (document.activeElement === focusedElement) {
301
+ focusedElement.blur()
302
+ }
303
+ }
304
+
305
+ // Set aria-hidden after ensuring no focus is inside the modal
306
+ this._element.setAttribute('aria-hidden', 'true')
307
+ const hiddenEvent = new CustomEvent(EVENT_HIDDEN, { bubbles: true })
308
+ this._element.dispatchEvent(hiddenEvent)
309
+ })
310
+ }
311
+
312
+ _isAnimated() {
313
+ return this._element.classList.contains(CLASS_NAME_FADE)
314
+ }
315
+
316
+ _triggerBackdropTransition() {
317
+ const hideEvent = new CustomEvent(EVENT_HIDE_PREVENTED, {
318
+ bubbles: true,
319
+ cancelable: true
320
+ })
321
+ this._element.dispatchEvent(hideEvent)
322
+ if (hideEvent.defaultPrevented) {
323
+ return
324
+ }
325
+
326
+ const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
327
+ const initialOverflowY = this._element.style.overflowY
328
+ // return if the following background transition hasn't yet completed
329
+ if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {
330
+ return
331
+ }
332
+
333
+ if (!isModalOverflowing) {
334
+ this._element.style.overflowY = 'hidden'
335
+ }
336
+
337
+ this._element.classList.add(CLASS_NAME_STATIC)
338
+ this._queueCallback(() => {
339
+ this._element.classList.remove(CLASS_NAME_STATIC)
340
+ this._queueCallback(() => {
341
+ this._element.style.overflowY = initialOverflowY
342
+ }, this._dialog)
343
+ }, this._dialog)
344
+
345
+ this._element.focus()
346
+ }
347
+
348
+ /**
349
+ * The following methods are used to handle overflowing modals
350
+ */
351
+
352
+ _adjustDialog() {
353
+ const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
354
+ const scrollbarWidth = this._scrollBar.getWidth()
355
+ const isBodyOverflowing = scrollbarWidth > 0
356
+
357
+ if (isBodyOverflowing && !isModalOverflowing) {
358
+ const property = isRTL() ? 'paddingLeft' : 'paddingRight'
359
+ this._element.style[property] = `${scrollbarWidth}px`
360
+ }
361
+
362
+ if (!isBodyOverflowing && isModalOverflowing) {
363
+ const property = isRTL() ? 'paddingRight' : 'paddingLeft'
364
+ this._element.style[property] = `${scrollbarWidth}px`
365
+ }
366
+ }
367
+
368
+ _resetAdjustments() {
369
+ this._element.style.paddingLeft = ''
370
+ this._element.style.paddingRight = ''
371
+ }
372
+
373
+ // Static
374
+ static jQueryInterface(config, relatedTarget) {
375
+ return this.each(function () {
376
+ const data = Modal.getOrCreateInstance(this, config)
377
+
378
+ if (typeof config !== 'string') {
379
+ return
380
+ }
381
+
382
+ if (typeof data[config] === 'undefined') {
383
+ throw new TypeError(`No method named "${config}"`)
384
+ }
385
+
386
+ data[config](relatedTarget)
387
+ })
388
+ }
389
+
390
+ static getOrCreateInstance(element, config = {}) {
391
+ if (!element._baseLineComponent) {
392
+ element._baseLineComponent = new Modal(element, config)
393
+ }
394
+ return element._baseLineComponent
395
+ }
396
+
397
+ static getInstance(element) {
398
+ return element._baseLineComponent || null
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Data API implementation
404
+ */
405
+
406
+ on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
407
+ const target = SelectorEngine.getElementFromSelector(this)
408
+
409
+ if (['A', 'AREA'].includes(this.tagName)) {
410
+ event.preventDefault()
411
+ }
412
+
413
+ one(target, EVENT_SHOW, (showEvent) => {
414
+ if (showEvent.defaultPrevented) {
415
+ // only register focus restorer if modal will actually get shown
416
+ return
417
+ }
418
+
419
+ one(target, EVENT_HIDDEN, () => {
420
+ if (isVisible(this)) {
421
+ this.focus()
422
+ }
423
+ })
424
+ })
425
+
426
+ // avoid conflict when clicking modal toggler while another one is open
427
+ const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)
428
+ if (alreadyOpen) {
429
+ Modal.getInstance(alreadyOpen).hide()
430
+ }
431
+
432
+ const data = Modal.getOrCreateInstance(target)
433
+
434
+ data.toggle(this)
435
+ })
436
+
437
+ enableDismissTrigger(Modal)
438
+
439
+ export default Modal
440
+