diff --git a/js/src/dropdown.js b/js/src/dropdown.js index a7a77cb3b6..e52c51e4db 100644 --- a/js/src/dropdown.js +++ b/js/src/dropdown.js @@ -194,6 +194,52 @@ class Dropdown { .trigger($.Event(Event.SHOWN, relatedTarget)) } + show() { + if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED) || $(this._menu).hasClass(ClassName.SHOW)) { + return + } + + const relatedTarget = { + relatedTarget: this._element + } + const showEvent = $.Event(Event.SHOW, relatedTarget) + + const parent = Dropdown._getParentFromElement(this._element) + $(parent).trigger(showEvent) + + if (showEvent.isDefaultPrevented()) { + return + } + + $(this._menu).toggleClass(ClassName.SHOW) + $(parent) + .toggleClass(ClassName.SHOW) + .trigger($.Event(Event.SHOWN, relatedTarget)) + } + + hide() { + if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED) || !$(this._menu).hasClass(ClassName.SHOW)) { + return + } + + const relatedTarget = { + relatedTarget: this._element + } + const hideEvent = $.Event(Event.HIDE, relatedTarget) + + const parent = Dropdown._getParentFromElement(this._element) + $(parent).trigger(hideEvent) + + if (hideEvent.isDefaultPrevented()) { + return + } + + $(this._menu).toggleClass(ClassName.SHOW) + $(parent) + .toggleClass(ClassName.SHOW) + .trigger($.Event(Event.HIDDEN, relatedTarget)) + } + dispose() { $.removeData(this._element, DATA_KEY) $(this._element).off(EVENT_KEY) diff --git a/js/tests/unit/dropdown.js b/js/tests/unit/dropdown.js index 40489c5f2c..fd9b7f9629 100644 --- a/js/tests/unit/dropdown.js +++ b/js/tests/unit/dropdown.js @@ -1097,4 +1097,268 @@ $(function () { assert.ok(dropdown._menu === null) assert.ok(dropdown._element === null) }) + + QUnit.test('should show dropdown', function (assert) { + assert.expect(2) + + var dropdownHTML = + '' + + var $dropdown = $(dropdownHTML) + .appendTo('#qunit-fixture') + .find('[data-toggle="dropdown"]') + .bootstrapDropdown() + + var dropdown = $dropdown.data('bs.dropdown') + var done = assert.async() + + $dropdown + .parent('.dropdown') + .on('show.bs.dropdown', function () { + assert.ok(true, 'show was fired') + }) + .on('shown.bs.dropdown', function () { + assert.ok($dropdown.parent('.dropdown').hasClass('show'), 'dropdown menu is shown') + done() + }) + + dropdown.show() + }) + + QUnit.test('should hide dropdown', function (assert) { + assert.expect(2) + + var dropdownHTML = + '' + + var $dropdown = $(dropdownHTML) + .appendTo('#qunit-fixture') + .find('[data-toggle="dropdown"]') + .bootstrapDropdown() + + var dropdown = $dropdown.data('bs.dropdown') + var done = assert.async() + $dropdown.trigger('click') + + $dropdown + .parent('.dropdown') + .on('hide.bs.dropdown', function () { + assert.ok(true, 'hide was fired') + }) + .on('hidden.bs.dropdown', function () { + assert.ok(!$dropdown.parent('.dropdown').hasClass('show'), 'dropdown menu is hidden') + done() + }) + + dropdown.hide() + }) + + QUnit.test('should not hide dropdown', function (assert) { + assert.expect(1) + + var dropdownHTML = + '' + + var $dropdown = $(dropdownHTML) + .appendTo('#qunit-fixture') + .find('[data-toggle="dropdown"]') + .bootstrapDropdown() + + var dropdown = $dropdown.data('bs.dropdown') + $dropdown.trigger('click') + dropdown.show() + + assert.ok($dropdown.parent('.dropdown').hasClass('show'), 'dropdown menu is still shown') + }) + + QUnit.test('should not show dropdown', function (assert) { + assert.expect(1) + + var dropdownHTML = + '' + + var $dropdown = $(dropdownHTML) + .appendTo('#qunit-fixture') + .find('[data-toggle="dropdown"]') + .bootstrapDropdown() + + var dropdown = $dropdown.data('bs.dropdown') + dropdown.hide() + assert.ok(!$dropdown.parent('.dropdown').hasClass('show'), 'dropdown menu is still hidden') + }) + + QUnit.test('should show dropdown', function (assert) { + assert.expect(2) + + var dropdownHTML = + '' + + var $dropdown = $(dropdownHTML) + .appendTo('#qunit-fixture') + .find('[data-toggle="dropdown"]') + .bootstrapDropdown() + + var dropdown = $dropdown.data('bs.dropdown') + var done = assert.async() + + $dropdown + .parent('.dropdown') + .on('show.bs.dropdown', function () { + assert.ok(true, 'show was fired') + }) + .on('shown.bs.dropdown', function () { + assert.ok($dropdown.parent('.dropdown').hasClass('show'), 'dropdown menu is shown') + done() + }) + + dropdown.show() + }) + + QUnit.test('should prevent default event on show method call', function (assert) { + assert.expect(1) + + var dropdownHTML = + '' + + var $dropdown = $(dropdownHTML) + .appendTo('#qunit-fixture') + .find('[data-toggle="dropdown"]') + .bootstrapDropdown() + + var dropdown = $dropdown.data('bs.dropdown') + var done = assert.async() + + $dropdown + .parent('.dropdown') + .on('show.bs.dropdown', function (event) { + event.preventDefault() + done() + }) + + dropdown.show() + assert.ok(!$dropdown.parent('.dropdown').hasClass('show'), 'dropdown menu is hidden') + }) + + QUnit.test('should prevent default event on hide method call', function (assert) { + assert.expect(1) + + var dropdownHTML = + '' + + var $dropdown = $(dropdownHTML) + .appendTo('#qunit-fixture') + .find('[data-toggle="dropdown"]') + .bootstrapDropdown() + + var dropdown = $dropdown.data('bs.dropdown') + var done = assert.async() + $dropdown.trigger('click') + + $dropdown + .parent('.dropdown') + .on('hide.bs.dropdown', function (event) { + event.preventDefault() + done() + }) + + dropdown.hide() + assert.ok($dropdown.parent('.dropdown').hasClass('show'), 'dropdown menu is shown') + }) + + QUnit.test('should not open dropdown via show method if target is disabled via attribute', function (assert) { + assert.expect(1) + var dropdownHTML = + '' + $(dropdownHTML).appendTo('#qunit-fixture') + var $dropdown = $('#qunit-fixture').find('[data-toggle="dropdown"]').bootstrapDropdown() + $dropdown.show() + assert.ok(!$dropdown.parent('.dropdown').hasClass('show')) + }) + + QUnit.test('should not open dropdown via show method if target is disabled via class', function (assert) { + assert.expect(1) + var dropdownHTML = + '' + + $(dropdownHTML).appendTo('#qunit-fixture') + var $dropdown = $('#qunit-fixture').find('[data-toggle="dropdown"]').bootstrapDropdown() + $dropdown.show() + assert.ok(!$dropdown.parent('.dropdown').hasClass('show')) + }) + + QUnit.test('should not hide dropdown via hide method if target is disabled via attribute', function (assert) { + assert.expect(1) + var dropdownHTML = + '' + $(dropdownHTML).appendTo('#qunit-fixture') + var $dropdown = $('#qunit-fixture').find('[data-toggle="dropdown"]').bootstrapDropdown() + $dropdown.hide() + assert.ok($dropdown.parent('.dropdown').hasClass('show')) + }) + + QUnit.test('should not hide dropdown via hide method if target is disabled via class', function (assert) { + assert.expect(1) + var dropdownHTML = + '' + + $(dropdownHTML).appendTo('#qunit-fixture') + var $dropdown = $('#qunit-fixture').find('[data-toggle="dropdown"]').bootstrapDropdown() + $dropdown.hide() + assert.ok($dropdown.parent('.dropdown').hasClass('show')) + }) }) diff --git a/site/docs/4.1/components/dropdowns.md b/site/docs/4.1/components/dropdowns.md index c9daf55186..ab61390d4e 100644 --- a/site/docs/4.1/components/dropdowns.md +++ b/site/docs/4.1/components/dropdowns.md @@ -839,6 +839,8 @@ Note when `boundary` is set to any value other than `'scrollParent'`, the style | Method | Description | | --- | --- | | `$().dropdown('toggle')` | Toggles the dropdown menu of a given navbar or tabbed navigation. | +| `$().dropdown('show')` | Shows the dropdown menu of a given navbar or tabbed navigation. | +| `$().dropdown('hide')` | Hides the dropdown menu of a given navbar or tabbed navigation. | | `$().dropdown('update')` | Updates the position of an element's dropdown. | | `$().dropdown('dispose')` | Destroys an element's dropdown. |