All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dominokit.domino.ui.utils.KeyboardNavigation Maven / Gradle / Ivy

/*
 * Copyright © 2019 Dominokit
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.dominokit.domino.ui.utils;

import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static org.dominokit.domino.ui.utils.Domino.*;
import static org.dominokit.domino.ui.utils.ElementUtil.isArrowDown;
import static org.dominokit.domino.ui.utils.ElementUtil.isArrowUp;
import static org.dominokit.domino.ui.utils.ElementUtil.isEnterKey;
import static org.dominokit.domino.ui.utils.ElementUtil.isEscapeKey;
import static org.dominokit.domino.ui.utils.ElementUtil.isSpaceKey;
import static org.dominokit.domino.ui.utils.ElementUtil.isTabKey;

import elemental2.dom.Event;
import elemental2.dom.EventListener;
import elemental2.dom.HTMLElement;
import elemental2.dom.KeyboardEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import jsinterop.base.Js;
import org.dominokit.domino.ui.IsElement;

/**
 * The `KeyboardNavigation` class provides a convenient way to handle keyboard navigation within a
 * list of elements.
 *
 * @param  The type of elements to navigate.
 */
public class KeyboardNavigation> implements EventListener {

  private final List items;
  private FocusHandler focusHandler;
  private ItemNavigationHandler selectHandler = (event, item) -> {};
  private ItemNavigationHandler enterHandler;
  private ItemNavigationHandler tabHandler;
  private ItemNavigationHandler spaceHandler;
  private final Map>> navigationHandlers = new HashMap<>();
  private FocusCondition focusCondition;
  private EscapeHandler escapeHandler;
  private EventOptions globalOptions = new EventOptions(true, true);
  private EventOptions enterOptions = new EventOptions(true, true);
  private EventOptions tabOptions = new EventOptions(true, true);
  private EventOptions spaceOptions = new EventOptions(true, true);
  private Consumer> onEndReached = (navigation) -> focusTopFocusableItem();
  private Consumer> onStartReached =
      (navigation) -> focusBottomFocusableItem();

  /**
   * Creates a new `KeyboardNavigation` instance for the given list of items.
   *
   * @param items The list of items to navigate.
   */
  public KeyboardNavigation(List items) {
    this.items = items;
  }

  /**
   * Creates a new `KeyboardNavigation` instance for the given list of items.
   *
   * @param items The list of items to navigate.
   * @param  The type of elements to navigate.
   * @return A new `KeyboardNavigation` instance.
   */
  public static > KeyboardNavigation create(List items) {
    return new KeyboardNavigation<>(items);
  }

  /**
   * Sets the focus handler for the keyboard navigation.
   *
   * @param focusHandler The focus handler to set.
   * @return This `KeyboardNavigation` instance.
   */
  public KeyboardNavigation onFocus(FocusHandler focusHandler) {
    this.focusHandler = focusHandler;
    return this;
  }

  /**
   * Sets the select handler for the keyboard navigation.
   *
   * @param selectHandler The select handler to set.
   * @return This `KeyboardNavigation` instance.
   */
  public KeyboardNavigation onSelect(ItemNavigationHandler selectHandler) {
    this.selectHandler = selectHandler;
    return this;
  }

  /**
   * Sets the escape handler for the keyboard navigation.
   *
   * @param escapeHandler The escape handler to set.
   * @return This `KeyboardNavigation` instance.
   */
  public KeyboardNavigation onEscape(EscapeHandler escapeHandler) {
    this.escapeHandler = escapeHandler;
    return this;
  }

  /**
   * Sets the focus condition for determining whether an element should receive focus.
   *
   * @param focusCondition The focus condition to set.
   * @return This `KeyboardNavigation` instance.
   */
  public KeyboardNavigation focusCondition(FocusCondition focusCondition) {
    this.focusCondition = focusCondition;
    return this;
  }

  /**
   * Handles keyboard events and navigates through the list of items accordingly.
   *
   * @param evt The keyboard event.
   */
  @Override
  public void handleEvent(Event evt) {
    KeyboardEvent keyboardEvent = (KeyboardEvent) evt;

    HTMLElement element = Js.uncheckedCast(keyboardEvent.target);
    for (V item : items) {
      if (item.element().contains(element)) {
        if (isArrowUp(keyboardEvent)) {
          doEvent(evt, globalOptions, () -> focusPrevious(item));
        } else if (isArrowDown(keyboardEvent)) {
          doEvent(evt, globalOptions, () -> focusNext(item));
        } else if (isEscapeKey(keyboardEvent)) {
          doEvent(evt, globalOptions, () -> escapeHandler.onEscape());
        }

        if (isEnterKey(keyboardEvent)) {
          doEvent(keyboardEvent, enterOptions, () -> onEnter(keyboardEvent, item));
        }

        if (isSpaceKey(keyboardEvent)) {
          doEvent(keyboardEvent, spaceOptions, () -> onSpace(keyboardEvent, item));
        }

        if (isTabKey(keyboardEvent)) {
          doEvent(keyboardEvent, tabOptions, () -> onTab(keyboardEvent, item));
        }

        onCustomHandler(keyboardEvent, item);
      }
    }
  }

  private void onCustomHandler(KeyboardEvent event, V item) {
    if (navigationHandlers.containsKey(event.key.toLowerCase())) {
      navigationHandlers
          .get(event.key.toLowerCase())
          .forEach(handler -> handler.onItemNavigation(event, item));
    }
  }

  private void onEnter(KeyboardEvent event, V item) {
    (nonNull(enterHandler) ? enterHandler : selectHandler).onItemNavigation(event, item);
  }

  private void onSpace(KeyboardEvent event, V item) {
    (nonNull(spaceHandler) ? spaceHandler : selectHandler).onItemNavigation(event, item);
  }

  private void onTab(KeyboardEvent event, V item) {
    (nonNull(tabHandler) ? tabHandler : selectHandler).onItemNavigation(event, item);
  }

  /**
   * Sets the enter handler for handling Enter key presses.
   *
   * @param enterHandler The enter handler to set.
   * @return This `KeyboardNavigation` instance.
   */
  public KeyboardNavigation setEnterHandler(ItemNavigationHandler enterHandler) {
    this.enterHandler = enterHandler;
    return this;
  }

  /**
   * Sets the tab handler for handling Tab key presses.
   *
   * @param tabHandler The tab handler to set.
   * @return This `KeyboardNavigation` instance.
   */
  public KeyboardNavigation setTabHandler(ItemNavigationHandler tabHandler) {
    this.tabHandler = tabHandler;
    return this;
  }

  /**
   * Sets the space handler for handling Space key presses.
   *
   * @param spaceHandler The space handler to set.
   * @return This `KeyboardNavigation` instance.
   */
  public KeyboardNavigation setSpaceHandler(ItemNavigationHandler spaceHandler) {
    this.spaceHandler = spaceHandler;
    return this;
  }

  /**
   * Sets the global options for event handling.
   *
   * @param globalOptions The global event options to set.
   * @return This `KeyboardNavigation` instance.
   */
  public KeyboardNavigation setGlobalOptions(EventOptions globalOptions) {
    this.globalOptions = globalOptions;
    return this;
  }

  /**
   * Sets the enter options for handling Enter key presses.
   *
   * @param enterOptions The enter event options to set.
   * @return This `KeyboardNavigation` instance.
   */
  public KeyboardNavigation setEnterOptions(EventOptions enterOptions) {
    this.enterOptions = enterOptions;
    return this;
  }

  /**
   * Sets the tab options for handling Tab key presses.
   *
   * @param tabOptions The tab event options to set.
   * @return This `KeyboardNavigation` instance.
   */
  public KeyboardNavigation setTabOptions(EventOptions tabOptions) {
    this.tabOptions = tabOptions;
    return this;
  }

  /**
   * Sets the space options for handling Space key presses.
   *
   * @param spaceOptions The space event options to set.
   * @return This `KeyboardNavigation` instance.
   */
  public KeyboardNavigation setSpaceOptions(EventOptions spaceOptions) {
    this.spaceOptions = spaceOptions;
    return this;
  }

  /**
   * Focuses on the next item in the list.
   *
   * @param item The current item.
   */
  public void focusNext(V item) {
    int nextIndex = items.indexOf(item) + 1;
    int size = items.size();
    if (nextIndex >= size) {
      onEndReached.accept(this);
    } else {
      for (int i = nextIndex; i < size; i++) {
        V itemToFocus = items.get(i);
        if (shouldFocus((V) itemToFocus)) {
          doFocus(itemToFocus);
          return;
        }
      }
      onEndReached.accept(this);
    }
  }

  /**
   * Checks if the given item is the last focusable item in the list.
   *
   * @param item The item to check.
   * @return `true` if the item is the last focusable item, `false` otherwise.
   */
  public boolean isLastFocusableItem(V item) {
    int nextIndex = items.indexOf(item) + 1;
    int size = items.size();
    if (nextIndex >= size) {
      return true;
    } else {
      return !items.subList(nextIndex, size).stream().anyMatch(this::shouldFocus);
    }
  }

  private boolean shouldFocus(V itemToFocus) {
    return isNull(focusCondition)
        || focusCondition.shouldFocus(itemToFocus)
            && !ElementsFactory.elements.elementOf(itemToFocus.element()).isHidden();
  }

  /** Focuses on the first focusable item in the list. */
  public void focusTopFocusableItem() {
    for (V item : items) {
      if (shouldFocus(item)) {
        doFocus(item);
        break;
      }
    }
  }

  /**
   * Get the first focusable item oif exists
   *
   * @return Optional of the first focusable item.
   */
  public Optional getTopFocusableItem() {
    for (V item : items) {
      if (shouldFocus(item)) {
        return Optional.of(item);
      }
    }
    return Optional.empty();
  }

  /** Focuses on the last focusable item in the list. */
  public void focusBottomFocusableItem() {
    for (int i = items.size() - 1; i >= 0; i--) {
      V itemToFocus = items.get(i);
      if (shouldFocus(itemToFocus)) {
        doFocus(itemToFocus);
        break;
      }
    }
  }

  /**
   * Focuses on the previous item in the list.
   *
   * @param item The current item.
   */
  public void focusPrevious(V item) {
    int nextIndex = items.indexOf(item) - 1;
    if (nextIndex < 0) {
      onStartReached.accept(this);
    } else {
      for (int i = nextIndex; i >= 0; i--) {
        V itemToFocus = items.get(i);
        if (shouldFocus(itemToFocus)) {
          doFocus(itemToFocus);
          return;
        }
      }
      onStartReached.accept(this);
    }
  }

  private void doFocus(V item) {
    focusHandler.doFocus(item);
  }

  /**
   * Focuses on the item at the specified index in the list.
   *
   * @param index The index of the item to focus.
   */
  public void focusAt(int index) {
    if (!items.isEmpty()) {
      V item = items.get(index);
      doFocus(item);
    }
  }

  private void doEvent(Event event, EventOptions options, EventExecutor executor) {
    if (options.stopPropagation) {
      event.stopPropagation();
    }
    executor.execute();
    if (options.preventDefault) {
      event.preventDefault();
    }
  }

  /**
   * Registers a custom navigation handler for a specific key code.
   *
   * @param keyCode The key code for which to register the handler.
   * @param navigationHandler The navigation handler to register.
   * @return This `KeyboardNavigation` instance.
   */
  public KeyboardNavigation registerNavigationHandler(
      String keyCode, ItemNavigationHandler navigationHandler) {
    if (!navigationHandlers.containsKey(keyCode)) {
      navigationHandlers.put(keyCode.toLowerCase(), new ArrayList<>());
    }
    navigationHandlers.get(keyCode.toLowerCase()).add(navigationHandler);
    return this;
  }

  /**
   * Removes a custom navigation handler for a specific key code.
   *
   * @param keyCode The key code for which to remove the handler.
   * @param navigationHandler The navigation handler to remove.
   * @return This `KeyboardNavigation` instance.
   */
  public KeyboardNavigation removeNavigationHandler(
      String keyCode, ItemNavigationHandler navigationHandler) {
    if (navigationHandlers.containsKey(keyCode.toLowerCase())) {
      navigationHandlers.get(keyCode.toLowerCase()).remove(navigationHandler);
    }
    return this;
  }

  /**
   * Use to change the behavior of navigating to the next item when navigating away from the last
   * item down by default this will focus the first focusable item in the list
   *
   * @param onEndReached a function for the new desired behavior.
   * @return Same KeyboardNavigation instance
   */
  public KeyboardNavigation setOnEndReached(Consumer> onEndReached) {
    this.onEndReached = onEndReached;
    return this;
  }

  /**
   * Use to change the behavior of navigating to the previous item when navigating away from the
   * first up item by default this will focus the last focusable item in the list
   *
   * @param onStartReached a function for the new desired behavior.
   * @return Same KeyboardNavigation instance
   */
  public KeyboardNavigation setOnStartReached(Consumer> onStartReached) {
    this.onStartReached = onStartReached;
    return this;
  }

  /**
   * A functional interface for handling focus on items.
   *
   * @param  The type of elements to focus.
   */
  @FunctionalInterface
  public interface FocusHandler {
    /**
     * Handles focusing on the given item.
     *
     * @param item The item to focus.
     */
    void doFocus(V item);
  }

  /**
   * A functional interface for handling item navigation.
   *
   * @param  The type of elements to navigate.
   */
  @FunctionalInterface
  public interface ItemNavigationHandler {
    /**
     * Handles item navigation based on a keyboard event.
     *
     * @param event The keyboard event.
     * @param item The item to navigate.
     */
    void onItemNavigation(KeyboardEvent event, V item);
  }

  /** A functional interface for handling Escape key presses. */
  @FunctionalInterface
  public interface EscapeHandler {
    /** Handles Escape key presses. */
    void onEscape();
  }

  /**
   * A functional interface for defining a focus condition for items.
   *
   * @param  The type of elements to focus.
   */
  @FunctionalInterface
  public interface FocusCondition {
    /**
     * Checks if the given item should receive focus.
     *
     * @param item The item to check.
     * @return `true` if the item should receive focus, `false` otherwise.
     */
    boolean shouldFocus(V item);
  }

  /** Represents event options for handling keyboard events. */
  private interface EventExecutor {
    void execute();
  }

  /** Represents event options for handling keyboard events. */
  public static final class EventOptions {
    private boolean preventDefault;
    private boolean stopPropagation;

    /**
     * Creates a new `EventOptions` instance with the specified options.
     *
     * @param preventDefault Indicates whether to prevent the default behavior of the event.
     * @param stopPropagation Indicates whether to stop the propagation of the event.
     */
    public EventOptions(boolean preventDefault, boolean stopPropagation) {
      this.preventDefault = preventDefault;
      this.stopPropagation = stopPropagation;
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy