diff --git a/css/builder.css b/css/builder.css
index fd56d241a8902bec7b895d7fe64e803eef742cb4..a7b7cbb06a7afdda38207173f163644142e25bec 100644
--- a/css/builder.css
+++ b/css/builder.css
@@ -29,7 +29,8 @@
 .lpb-empty-container__wrapper .lpb-section-menu__wrapper {
   bottom: 20px;
 }
-.is-dragging .js-lpb-region {
+.is-dragging .js-lpb-region,
+.is-navigating .js-lpb-region {
   outline: 1px dotted blue;
 }
 .is-dragging .t-reversed .js-lpb-region {
@@ -41,10 +42,13 @@
   transition: all .15s linear;
   position: relative;
 }
-.js-lpb-component:hover,
-.js-lpb-component:focus-within {
+.lp-builder:not(.is-navigating) .js-lpb-component:hover,
+.lp-builder:not(.is-navigating) .js-lpb-component:focus-within {
   outline: 1px solid blue;
 }
+.js-lpb-component.is-navigating {
+  outline: 3px solid blue;
+}
 .js-lpb-component {
   cursor: grab;
 }
@@ -52,13 +56,10 @@
 .js-lpb-component:focus-within .js-lpb-region {
   outline: 1px dotted rgba(0, 0, 255, 0.5);
 }
-.js-lpb-component:hover .js-lpb-region:hover,
-.js-lpb-component:focus-within .js-lpb-region:focus-within {
+.lp-builder:not(.is-navigating) .js-lpb-component:hover .js-lpb-region:hover,
+.lp-builder:not(.is-navigating) .js-lpb-component:focus-within .js-lpb-region:focus-within {
   outline: 1px solid rgba(0, 0, 255, 0.5);
 }
-.js-lpb-component:hover .js-lpb-active-item {
-  outline: 1px solid blue !important;
-}
 .lpb-layout {
   padding: 20px;
 }
@@ -69,10 +70,6 @@
   padding: 5px 10px;
   font-size: small;
 }
-.lpb-layout.lpb-active-item {
-  outline: 3px solid #fff !important;
-  box-shadow: 0 0 0 4px rgba(0, 0, 255, 1);
-}
 .js-lpb-btn--add {
   position: absolute;
 }
@@ -91,6 +88,10 @@
   box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.25), 0px 1px 3px rgba(0, 0, 0, 0.25);
   background-color: #fff;
 }
+.lpb-controls:hover,
+.lpb-controls:focus-within {
+  z-index: 1010;
+}
 .lpb-controls.is-layout {
   background-color: #00659B;
   border-radius: 6px 6px 0px 0px;
@@ -100,8 +101,8 @@
   left: default;
   right: -1px;
 }
-.js-lpb-component:hover > .lpb-controls,
-.js-lpb-component:focus-within > .lpb-controls {
+.lp-builder:not(.is-navigating) .js-lpb-component:hover > .lpb-controls,
+.lp-builder:not(.is-navigating) .js-lpb-component:focus-within > .lpb-controls {
   opacity: 1;
 }
 .lpb-controls a span {
@@ -231,8 +232,8 @@
 .js-lpb-component > .lpb-btn {
   opacity: 0;
 }
-.js-lpb-component:hover > .lpb-btn,
-.js-lpb-component:focus-within > .lpb-btn {
+.lp-builder:not(.is-navigating) .js-lpb-component:hover > .lpb-btn,
+.lp-builder:not(.is-navigating) .js-lpb-component:focus-within > .lpb-btn {
   opacity: 1;
 }
 .lpb-btn--add {
@@ -257,6 +258,12 @@
   background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M13 6V11H18V13H13V18H11V12.999L6 13V11L11 10.999V6H13Z' fill='%23130F13'/%3E%3C/svg%3E%0A");
   opacity: 1;
 }
+.lpb-btn--add:hover,
+.lpb-btn:hover,
+.lpb-btn--add:focus-within,
+.lpb-btn:focus-within {
+  z-index: 1010;
+}
 .js-lpb-component .lpb-btn--add,
 .js-lpb-region .lpb-btn--add {
   opacity: 0;
@@ -275,13 +282,13 @@
 .lpb-btn--add.after {
   bottom: -16px;
 }
-.js-lpb-component:hover > .lpb-btn--add,
-.js-lpb-component:focus-within > .lpb-btn--add {
+.lp-builder:not(.is-navigating) .js-lpb-component:hover > .lpb-btn--add,
+.lp-builder:not(.is-navigating) .js-lpb-component:focus-within > .lpb-btn--add {
   visibility: visible;
   opacity: 1;
 }
-.js-lpb-region:hover > .lpb-btn--add,
-.js-lpb-region:focus-within > .lpb-btn--add {
+.lp-builder:not(.is-navigating) .js-lpb-region:hover > .lpb-btn--add,
+.lp-builder:not(.is-navigating) .js-lpb-region:focus-within > .lpb-btn--add {
   visibility: visible;
   opacity: 1;
 }
@@ -359,4 +366,43 @@ a.lpb-enable-button::before {
 .lpb-formatter:hover .lpb-enable,
 .lpb-formatter:focus-within .lpb-enable {
   opacity: 1;
+}
+
+.lpb-tooltiptext {
+  opacity: 0;
+  transition: all .15s linear;
+  width: auto;
+  background-color: black;
+  color: #fff;
+  text-align: center;
+  padding: 10px;
+  position: absolute;
+  z-index: 1000;
+  bottom: 100%;
+  left: -7px;
+  margin-bottom: -2px;
+  white-space: nowrap;
+}
+.lpb-tooltiptext::after {
+  content: " ";
+  position: absolute;
+  top: 100%; /* At the bottom of the tooltip */
+  left: 20px;
+  border-width: 7px;
+  border-style: solid;
+  border-color: black transparent transparent transparent;
+}
+.lp-builder:not(.is-dragging) .lpb-tooltip--hover:hover + .lpb-tooltiptext {
+  transition-delay: 1s;
+}
+.lp-builder:not(.is-dragging) .lpb-tooltip--focus:focus + .lpb-tooltiptext,
+.lp-builder:not(.is-dragging) .lpb-tooltip--hover:hover + .lpb-tooltiptext,
+.lpb-tooltiptext--visible{
+  opacity: 1;
+}
+.js-lpb-ui-message {
+  background-color: 000;
+  color: #fff;
+  text-align: center;
+  position: static;
 }
\ No newline at end of file
diff --git a/js/builder.js b/js/builder.js
index c34528cc0bfbc54e95f61e6bea3a6637d5dd01eb..6674903ff37a0863dd0e3fc7cc431260eebc1946 100644
--- a/js/builder.js
+++ b/js/builder.js
@@ -7,13 +7,15 @@
    *   The container.
    * @param {string} id
    *   The container id.
+   * @param {Object} settings
+   *   The settings object.
    */
   function attachUiElements($container, id, settings) {
     const lpbBuilderSettings = settings.lpBuilder || {};
     const uiElements = lpbBuilderSettings.uiElements || {};
     const containerUiElements = uiElements[id] || [];
-    containerUiElements.forEach((uiElement) => {
-      const {element, method} = uiElement;
+    containerUiElements.forEach(uiElement => {
+      const { element, method } = uiElement;
       $container[method](element);
     });
     Drupal.behaviors.AJAX.attach($container[0], drupalSettings);
@@ -138,7 +140,9 @@
           $moveItem.css({ transform: 'none' });
           $sibling.css({ transform: 'none' });
           $sibling[method]($moveItem);
-          $moveItem.closest(`[${idAttr}]`).trigger('lpb-component:move', [$moveItem.attr('data-uuid')]);
+          $moveItem
+            .closest(`[${idAttr}]`)
+            .trigger('lpb-component:move', [$moveItem.attr('data-uuid')]);
         },
       },
     );
@@ -160,7 +164,7 @@
     // Add shims as target elements.
     if (dir === -1) {
       $(
-        '.js-lpb-region .lpb-btn--add, .lpb-layout:not(.lpb-active-item)',
+        '.js-lpb-region .lpb-btn--add.center, .lpb-layout:not(.lpb-active-item)',
         $element,
       ).before('<div class="lpb-shim"></div>');
     } else if (dir === 1) {
@@ -198,7 +202,34 @@
     // Remove the shims and save the order.
     $('.lpb-shim', $element).remove();
     $item.removeClass('lpb-active-item').focus();
-    $item.closest(`[${idAttr}]`).trigger('lpb-component:move', [$item.attr('data-uuid')]);
+    $item
+      .closest(`[${idAttr}]`)
+      .trigger('lpb-component:move', [$item.attr('data-uuid')]);
+  }
+  function startNav($item) {
+    const $msg = $(
+      `<div id="lpb-navigating-msg" class="lpb-tooltiptext lpb-tooltiptext--visible js-lpb-tooltiptext">${Drupal.t(
+        'Use arrow keys to move. Press Return or Tab when finished.',
+      )}</div>`,
+    );
+    $item
+      .closest('.lp-builder')
+      .addClass('is-navigating')
+      .find('.is-navigating')
+      .removeClass('is-navigating');
+    $item
+      .attr('aria-describedby', 'lpb-navigating-msg')
+      .addClass('is-navigating')
+      .prepend($msg);
+  }
+  function stopNav($item) {
+    $item
+      .removeClass('is-navigating')
+      .closest('.lp-builder')
+      .removeClass('is-navigating')
+      .attr('aria-describedby', '')
+      .find('.js-lpb-tooltiptext')
+      .remove();
   }
   /**
    * Prevents user from navigating away and accidentally loosing changes.
@@ -246,14 +277,28 @@
       $(e.currentTarget).focus();
       return false;
     });
+    $element.on('click.lp-builder', '.lpb-drag', e => {
+      const $btn = $(e.currentTarget);
+      startNav($btn.closest('.js-lpb-component'));
+    });
     document.addEventListener('keydown', e => {
-      const $item = $('.js-lpb-component:focus');
+      const $item = $('.js-lpb-component.is-navigating');
       if ($item.length) {
-        if (e.code === 'ArrowDown' && $item) {
-          nav($item, 1, settings);
-        }
-        if (e.code === 'ArrowUp' && $item) {
-          nav($item, -1, settings);
+        switch (e.code) {
+          case 'ArrowUp':
+          case 'ArrowLeft':
+            nav($item, -1, settings);
+            break;
+          case 'ArrowDown':
+          case 'ArrowRight':
+            nav($item, 1, settings);
+            break;
+          case 'Enter':
+          case 'Tab':
+            stopNav($item);
+            break;
+          default:
+            break;
         }
       }
     });
@@ -345,11 +390,15 @@
       }
     }
   });
-  Drupal.AjaxCommands.prototype.LayoutParagraphsEventCommand = (ajax, response, status) => {
-    const {layoutId, componentUuid, eventName} = response;
+  Drupal.AjaxCommands.prototype.LayoutParagraphsEventCommand = (
+    ajax,
+    response,
+    status,
+  ) => {
+    const { layoutId, componentUuid, eventName } = response;
     const $element = $(`[data-lpb-id="${layoutId}"]`);
     $element.trigger(`lpb-${eventName}`, [componentUuid]);
-  }
+  };
   Drupal.behaviors.layoutParagraphsBuilder = {
     attach: function attach(context, settings) {
       // Initialize the editor ui.
@@ -380,22 +429,23 @@
         'lpb-component:update.lpb',
         'lpb-component:move.lpb',
         'lpb-component:drop.lpb',
-        'lpb-component:delete.lpb'
+        'lpb-component:delete.lpb',
       ].join(' ');
-      $('[data-lpb-id]').once('lpb-events').on(events, e => {
-        const $element = $(e.currentTarget);
-        updateUi($element);
-      });
+      $('[data-lpb-id]')
+        .once('lpb-events')
+        .on(events, e => {
+          const $element = $(e.currentTarget);
+          updateUi($element);
+        });
 
       // Add UI elements to the builder, each component, and each region.
-      [
-        `${idAttr}`,
-        'data-uuid',
-        'data-region-uuid',
-      ].forEach((attr) => {
-        $(`[${attr}]`).not('.has-components').once('lpb-ui-elements').each((i, el) => {
-          attachUiElements($(el), el.getAttribute(attr), settings);
-        });
+      [`${idAttr}`, 'data-uuid', 'data-region-uuid'].forEach(attr => {
+        $(`[${attr}]`)
+          .not('.has-components')
+          .once('lpb-ui-elements')
+          .each((i, el) => {
+            attachUiElements($(el), el.getAttribute(attr), settings);
+          });
       });
     },
   };
diff --git a/layout_paragraphs.module b/layout_paragraphs.module
index b9d83d007ab77bd605faa063c30cc6bf1a89e67c..2dc2109970501310658623a3f94695f0c6bc456f 100644
--- a/layout_paragraphs.module
+++ b/layout_paragraphs.module
@@ -61,6 +61,7 @@ function layout_paragraphs_theme() {
         'label' => NULL,
         'edit_attributes' => [],
         'delete_attributes' => [],
+        'unique_id' => [],
       ],
     ],
     'layout_paragraphs_builder_component_menu' => [
diff --git a/src/Element/LayoutParagraphsBuilder.php b/src/Element/LayoutParagraphsBuilder.php
index abe82fc98bf6eaf709bdc0c934a5e7fec2224a3c..7f3b25a42e1b8d2d17a4ad012e7e9015c09c4c52 100644
--- a/src/Element/LayoutParagraphsBuilder.php
+++ b/src/Element/LayoutParagraphsBuilder.php
@@ -2,25 +2,26 @@
 
 namespace Drupal\layout_paragraphs\Element;
 
-use Drupal\Component\Serialization\Json;
 use Drupal\Core\Url;
 use Drupal\Core\Render\Markup;
 use Drupal\Core\Render\Renderer;
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Template\Attribute;
+use Drupal\Component\Serialization\Json;
+use Drupal\paragraphs\ParagraphInterface;
 use Drupal\Core\Access\AccessResultAllowed;
 use Drupal\Core\Layout\LayoutPluginManager;
 use Drupal\Core\Entity\EntityTypeBundleInfo;
 use Drupal\Core\Access\AccessResultForbidden;
-use Drupal\Core\Entity\EntityRepositoryInterface;
 use Drupal\Core\Render\Element\RenderElement;
+use Drupal\layout_paragraphs\DialogHelperTrait;
+use Drupal\Core\Entity\EntityRepositoryInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-use Drupal\Core\Template\Attribute;
-use Drupal\paragraphs\ParagraphInterface;
 use Drupal\layout_paragraphs\LayoutParagraphsSection;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\layout_paragraphs\LayoutParagraphsComponent;
-use Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository;
-use Drupal\layout_paragraphs\DialogHelperTrait;
 use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\layout_paragraphs\LayoutParagraphsLayoutTempstoreRepository;
 
 /**
  * Defines a render element for building the Layout Builder UI.
@@ -332,7 +333,7 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP
     else {
       $delete_attributes = [];
     }
-    
+
     $controls = [
       '#theme' => 'layout_paragraphs_builder_controls',
       '#attributes' => [
@@ -343,6 +344,7 @@ class LayoutParagraphsBuilder extends RenderElement implements ContainerFactoryP
       '#label' => $entity->getParagraphType()->label,
       '#edit_attributes' => $edit_attributes,
       '#delete_attributes' => $delete_attributes,
+      '#unique_id' => Html::getUniqueId('lpb-controls'),
       '#weight' => -10001,
     ];
     if ($component->isLayout()) {
diff --git a/templates/layout-paragraphs-builder-controls.html.twig b/templates/layout-paragraphs-builder-controls.html.twig
index 5f957b0542a26abcde022b67db386b284162fcb3..96b0919ea9cd8a9991a91a578bdf4340d7ec2098 100644
--- a/templates/layout-paragraphs-builder-controls.html.twig
+++ b/templates/layout-paragraphs-builder-controls.html.twig
@@ -1,5 +1,6 @@
   <div{{ attributes }}>
-    <a class="lpb-drag" href="#nav"><span>{{ 'Nav'|t }}</span></a>
+    <a class="lpb-drag lpb-tooltip--hover lpb-tooltip--focus" aria-describedby="{{unique_id}}--tip" href="#nav"><span>{{ 'Nav'|t }}</span></a>
+    <span class="lpb-tooltiptext" id="{{unique_id}}--tip">{{ 'Drag or click and use arrow keys to move. <br />Press Return or Tab when finished.'|t }}</span>
     <span class="lpb-controls-label">{{label}}</span>
     <a class="lpb-up" href="#move-up"><span>{{ 'Move Up'|t }}</span></a>
     <a class="lpb-down" href="#move-down"><span>{{ 'Move Down'|t }}</span></a>
diff --git a/tests/src/FunctionalJavascript/BuilderTest.php b/tests/src/FunctionalJavascript/BuilderTest.php
index 9c18187ffe6d8cbb570a1b7cf59edf8f3a2ed04f..13433ee42bd471021b4a4e619ce10f250180e67d 100644
--- a/tests/src/FunctionalJavascript/BuilderTest.php
+++ b/tests/src/FunctionalJavascript/BuilderTest.php
@@ -117,36 +117,26 @@ class BuilderTest extends WebDriverTestBase {
     $third_col = $page->find('css', '.layout__region--third');
     $this->assertNotEmpty($third_col);
 
-    // Add a text item to first column.
-    // Because there are only two component types and sections cannot
-    // be nested, this will load the text component form directly.
-    $button = $page->find('css', '.layout__region--first .lpb-btn--add');
-    $button->click();
-    $this->assertSession()->assertWaitOnAjaxRequest();
-    $this->assertSession()->pageTextContains('field_text');
-
-    $page->fillField('field_text[0][value]', 'Some arbitrary text');
-    // Force show the hidden submit button so we can click it.
-    $this->getSession()->executeScript("jQuery('.lpb-btn--save').attr('style', '');");
-    $button = $this->assertSession()->waitForElementVisible('css', ".lpb-btn--save");
-    $button->press();
-
-    $this->assertSession()->assertWaitOnAjaxRequest();
-    $this->assertSession()->pageTextContains('Some arbitrary text');
+  }
 
+  /**
+   * Tests adding a component into a section.
+   */
+  public function testAddComponent() {
+    $this->testAddSection();
+    $this->addTextComponent('Some arbitrary text', '.layout__region--first .lpb-btn--add');
     $this->submitForm([
       'title[0][value]' => 'Node title',
     ], 'Save');
     $this->assertSession()->pageTextContains('Node title');
     $this->assertSession()->pageTextContains('Some arbitrary text');
-
   }
 
   /**
    * Tests editing a paragraph.
    */
   public function testEditComponent() {
-    $this->testAddSection();
+    $this->testAddComponent();
     $this->drupalGet('node/1/edit');
 
     $page = $this->getSession()->getPage();
@@ -161,35 +151,14 @@ class BuilderTest extends WebDriverTestBase {
    * Tests reordering components.
    */
   public function testReorderComponents() {
-    $this->testAddSection();
+    $this->testAddComponent();
     $this->drupalGet('node/1/edit');
 
     $page = $this->getSession()->getPage();
+    $this->addTextComponent('Second text item.', '[data-id="2"] .lpb-btn--add.after');
+    $this->assertOrderOfStrings(['Some arbitrary text', 'Second text item.'], 'Second item was not correctly added after the first.');
 
-    // Add a SECOND text item to first column.
-    // Because there are only two component types and sections cannot
-    // be nested, this will load the text component form directly.
-    $button = $page->find('css', '[data-id="2"] .lpb-btn--add.after');
-    $button->click();
-    $this->assertSession()->assertWaitOnAjaxRequest();
-    $this->assertSession()->pageTextContains('field_text');
-
-    $page->fillField('field_text[0][value]', 'Second text item.');
-    // Force show the hidden submit button so we can click it.
-    $this->getSession()->executeScript("jQuery('.lpb-btn--save').attr('style', '');");
-    $button = $this->assertSession()->waitForElementVisible('css', ".lpb-btn--save");
-    $button->press();
-    $this->assertSession()->assertWaitOnAjaxRequest(1000, 'Could not save new component.');
-
-    // Make sure the new component was added AFTER the existing one.
-    $page_text = $page->getHtml();
-    $pos1 = strpos($page_text, 'Second text item.');
-    $pos2 = strpos($page_text, 'Some arbitrary text');
-    if ($pos1 < $pos2) {
-      throw new ExpectationException("New component was incorrectly added above the existing one.", $this->getSession()->getDriver());
-    }
-
-    // Move the new item up above the first.
+    // Click the new item's move up button.
     $button = $page->find('css', '.is_new .lpb-up');
     $button->click();
 
@@ -200,19 +169,94 @@ class BuilderTest extends WebDriverTestBase {
     $this->assertSession()->pageTextContains('Second text item.');
 
     // The second component should now appear first in the page source.
-    $page_text = $page->getHtml();
-    $pos1 = strpos($page_text, 'Second text item.');
-    $pos2 = strpos($page_text, 'Some arbitrary text');
-    if ($pos1 > $pos2) {
-      throw new ExpectationException("Components were not correctly reordered.", $this->getSession()->getDriver());
-    }
+    $this->assertOrderOfStrings(['Second text item.', 'Some arbitrary text'], 'Components were not correctly reordered.');
+  }
+
+  /**
+   * Tests keyboard navigation.
+   */
+  public function testKeyboardNavigation() {
+
+    $this->testAddSection();
+    $page = $this->getSession()->getPage();
+    $this->submitForm([
+      'title[0][value]' => 'Node title',
+    ], 'Save');
+
+    $this->drupalGet('node/1/edit');
+    $this->addTextComponent('First item', '.layout__region--first .lpb-btn--add');
+    $this->addTextComponent('Second item', '.layout__region--second .lpb-btn--add');
+    $this->addTextComponent('Third item', '.layout__region--third .lpb-btn--add');
+
+    // Click the new item's drag button.
+    // This should create a <div> with the id 'lpb-navigatin-msg'.
+    $button = $page->find('css', '.layout__region--third .lpb-drag');
+    $button->click();
+    $this->assertSession()->elementExists('css', '#lpb-navigating-msg');
+
+    // Moves third item to bottom of second region.
+    $this->keyPress('ArrowUp');
+    $this->assertOrderOfStrings(['First item', 'Second item', 'Third item']);
+
+    // Moves third item to top of second region.
+    $this->keyPress('ArrowUp');
+    $this->assertOrderOfStrings(['First item', 'Third item', 'Second item']);
+
+    // Moves third item to bottom of first region.
+    $this->keyPress('ArrowUp');
+    $this->assertOrderOfStrings(['First item', 'Third item', 'Second item']);
 
+    // Moves third item to top of first region.
+    $this->keyPress('ArrowUp');
+    $this->assertOrderOfStrings(['Third item', 'First item', 'Second item']);
+
+    // Save the node.
+    $this->submitForm([
+      'title[0][value]' => 'Node title',
+    ], 'Save');
+
+    // Ensures reordering was correctly applied via Ajax.
+    $this->assertOrderOfStrings(['Third item', 'First item', 'Second item']);
   }
 
+  /**
+   * Uses Javascript to make a DOM element visible.
+   *
+   * @param string $selector
+   *   A css selector.
+   */
   protected function forceVisible($selector) {
     $this->getSession()->executeScript("jQuery('{$selector} .contextual .trigger').toggleClass('visually-hidden');");
   }
 
+  /**
+   * Inserts a text component by clicking the "+" button.
+   *
+   * @param string $text
+   *   The text for the component's field_text value.
+   * @param string $css_selector
+   *   A css selector targeting the "+" button.
+   */
+  protected function addTextComponent($text, $css_selector) {
+    $page = $this->getSession()->getPage();
+    // Add a text item to first column.
+    // Because there are only two component types and sections cannot
+    // be nested, this will load the text component form directly.
+    $button = $page->find('css', $css_selector);
+    $button->click();
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()->pageTextContains('field_text');
+
+    $page->fillField('field_text[0][value]', $text);
+    // Force show the hidden submit button so we can click it.
+    $this->getSession()->executeScript("jQuery('.lpb-btn--save').attr('style', '');");
+    $button = $this->assertSession()->waitForElementVisible('css', ".lpb-btn--save");
+    $button->press();
+
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()->pageTextContains($text);
+  }
+
   /**
    * Creates a new user with provided permissions and logs them in.
    *
@@ -228,4 +272,51 @@ class BuilderTest extends WebDriverTestBase {
     return $user;
   }
 
+  /**
+   * {@inheritDoc}
+   *
+   * Added method with fixed return comment for IDE type hinting.
+   *
+   * @return \Drupal\FunctionalJavascriptTests\JSWebAssert
+   *   A new JS web assert object.
+   */
+  public function assertSession($name = '') {
+    $js_web_assert = parent::assertSession($name);
+    return $js_web_assert;
+  }
+
+  /**
+   * Asserts that provided strings appear on page in same order as in array.
+   *
+   * @param array $strings
+   *   A list of strings in the order they are expected to appear.
+   * @param string $assert_message
+   *   Message if assertion fails.
+   */
+  protected function assertOrderOfStrings(array $strings, $assert_message = 'Strings are not in correct order.') {
+    $page = $this->getSession()->getPage();
+    $page_text = $page->getHtml();
+    $highmark = -1;
+    foreach ($strings as $string) {
+      $this->assertSession()->pageTextContains($string);
+      $pos = strpos($page_text, $string);
+      if ($pos <= $highmark) {
+        throw new ExpectationException($assert_message, $this->getSession()->getDriver());
+      }
+      $highmark = $pos;
+    }
+  }
+
+  /**
+   * Simulates pressing a key with javascript.
+   *
+   * @param string $key_code
+   *   The string key code (i.e. ArrowUp, Enter).
+   */
+  protected function keyPress($key_code) {
+    $script = 'var e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, code: "' . $key_code . '"});
+    document.body.dispatchEvent(e);';
+    $this->getSession()->executeScript($script);
+  }
+
 }