theme/boost/amd/src/bootstrap/util/focustrap.js

  1. /**
  2. * --------------------------------------------------------------------------
  3. * Bootstrap util/focustrap.js
  4. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
  5. * --------------------------------------------------------------------------
  6. */
  7. import EventHandler from '../dom/event-handler'
  8. import SelectorEngine from '../dom/selector-engine'
  9. import Config from './config'
  10. /**
  11. * Constants
  12. */
  13. const NAME = 'focustrap'
  14. const DATA_KEY = 'bs.focustrap'
  15. const EVENT_KEY = `.${DATA_KEY}`
  16. const EVENT_FOCUSIN = `focusin${EVENT_KEY}`
  17. const EVENT_KEYDOWN_TAB = `keydown.tab${EVENT_KEY}`
  18. const TAB_KEY = 'Tab'
  19. const TAB_NAV_FORWARD = 'forward'
  20. const TAB_NAV_BACKWARD = 'backward'
  21. const Default = {
  22. autofocus: true,
  23. trapElement: null // The element to trap focus inside of
  24. }
  25. const DefaultType = {
  26. autofocus: 'boolean',
  27. trapElement: 'element'
  28. }
  29. /**
  30. * Class definition
  31. */
  32. class FocusTrap extends Config {
  33. constructor(config) {
  34. super()
  35. this._config = this._getConfig(config)
  36. this._isActive = false
  37. this._lastTabNavDirection = null
  38. }
  39. // Getters
  40. static get Default() {
  41. return Default
  42. }
  43. static get DefaultType() {
  44. return DefaultType
  45. }
  46. static get NAME() {
  47. return NAME
  48. }
  49. // Public
  50. activate() {
  51. if (this._isActive) {
  52. return
  53. }
  54. if (this._config.autofocus) {
  55. this._config.trapElement.focus()
  56. }
  57. EventHandler.off(document, EVENT_KEY) // guard against infinite focus loop
  58. EventHandler.on(document, EVENT_FOCUSIN, event => this._handleFocusin(event))
  59. EventHandler.on(document, EVENT_KEYDOWN_TAB, event => this._handleKeydown(event))
  60. this._isActive = true
  61. }
  62. deactivate() {
  63. if (!this._isActive) {
  64. return
  65. }
  66. this._isActive = false
  67. EventHandler.off(document, EVENT_KEY)
  68. }
  69. // Private
  70. _handleFocusin(event) {
  71. const { trapElement } = this._config
  72. if (event.target === document || event.target === trapElement || trapElement.contains(event.target)) {
  73. return
  74. }
  75. const elements = SelectorEngine.focusableChildren(trapElement)
  76. if (elements.length === 0) {
  77. trapElement.focus()
  78. } else if (this._lastTabNavDirection === TAB_NAV_BACKWARD) {
  79. elements[elements.length - 1].focus()
  80. } else {
  81. elements[0].focus()
  82. }
  83. }
  84. _handleKeydown(event) {
  85. if (event.key !== TAB_KEY) {
  86. return
  87. }
  88. this._lastTabNavDirection = event.shiftKey ? TAB_NAV_BACKWARD : TAB_NAV_FORWARD
  89. }
  90. }
  91. export default FocusTrap