Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8342655

ScrollPane should not consume navigation keys when it doesn't have direct focus

XMLWordPrintable

    • Icon: CSR CSR
    • Resolution: Approved
    • Icon: P4 P4
    • jfx24
    • javafx
    • None
    • behavioral
    • low
    • Hide
      Custom control authors may have relied on the current behavior. After this change, they may find that using the directional keys will trigger directional navigation instead of scrolling of the viewport if their control was contained within a `ScrollPane`. A work-around is provided in the form of installing a custom event handler.
      Show
      Custom control authors may have relied on the current behavior. After this change, they may find that using the directional keys will trigger directional navigation instead of scrolling of the viewport if their control was contained within a `ScrollPane`. A work-around is provided in the form of installing a custom event handler.
    • Java API
    • JDK

      Summary

      ScrollPane will no longer act on keys to scroll its view port when it does not have the focus. Before this change, ScrollPane would consume keys that bubbled up from focused child controls, even though it did not have the focus. The behavior when the ScrollPane has direct focus is unchanged.

      Problem

      Users generally expect that the focused control is the one that will respond to key presses. A visual cue for this is present in the form of a highlight to indicate this. Users will also expect that controls will act similar regardless of their placement (within a ScrollPane or not). However, as ScrollPane would act on bubbled up keys without checking whether it had focus, pressing keys that users think are intended for the focused control may trigger an action in the surrounding scroll pane instead.

      This is especially apparent when designing a custom control. The custom control will function as any other JavaFX control, participating in both logical (tab) and directional navigation (arrow keys) as any FX control would. However, when such a control is placed within a ScrollPane this breaks down, and directional navigation is blocked by the scroll pane consuming these keys for scrolling the viewport.

      The standard FX controls are (mostly) unaffected by this, as they all explicitly handle most keys that ScrollPane acts on. For example, a Button installs navigation handlers for the arrow keys. It however would not have to, and should not have to, if it could let those keys bubble up safely to Scene where navigation concerns are supposed to be handled.

      Another indication that the current behavior is not that useful is that no FX controls that incorporate a scrollable area will delegate their default scrolling behavior to ScrollPane. This is because it is not aware of its actual content, and any scrolling that it provides would be unaware of page sizes, rows or columns, or any other unit that would make sense as good scroll unit.

      This change will open the way to removing navigational concerns from controls, and leaving this to Scene. As controls will no longer need to explicitly act on directional navigation keys, these can be left to bubble up, simplifying their implementations and also allows users to change the meaning of such keys more easily as controls no longer have to consume them early.

      Solution

      Modify the internal ScrollPaneBehavior to take focus into account before consuming key presses.

      For users that relied on the old behavior, installing an event handler on ScrollPane to handle the keys even when not focused is offered as a solution. For example:

          scrollPane.addEventFilter(KeyEvent.KEY_PRESSED, e -> {
              double x = 0;
              double y = 0;
      
              switch(e.getCode()) {
                  case KeyCode.LEFT -> x = -0.1;
                  case KeyCode.RIGHT -> x = 0.1;
                  case KeyCode.UP -> y = -0.1;
                  case KeyCode.DOWN -> y = 0.1;
                  case KeyCode.PAGE_UP -> y = -0.9;
                  case KeyCode.PAGE_DOWN -> y = 0.9;
                  case KeyCode.SPACE -> y = 0.9;
                  case KeyCode.HOME -> x = y = Double.NEGATIVE_INFINITY;
                  case KeyCode.END -> x = y = Double.POSITIVE_INFINITY;
                  default -> {}
              }
      
              if(x != 0 || y != 0) {
                  scrollByFraction(scrollPane, x, y);
                  e.consume();
              }
          });

      Which makes use of this helper function:

      static void scrollByFraction(ScrollPane scrollPane, double x, double y) {
          Node content = scrollPane.getContent();
      
          if (content == null) {
              return;
          }
      
          Bounds viewportBounds = scrollPane.getViewportBounds();
          Bounds layoutBounds = content.getLayoutBounds();
      
          if (x != 0) {
              double visibleFraction = viewportBounds.getWidth() / layoutBounds.getWidth();
              double range = scrollPane.getHmax() - scrollPane.getHmin();
              double scrollFactor = range * visibleFraction / (1 - visibleFraction);
      
              scrollPane.setHvalue(scrollPane.getHvalue() + x * scrollFactor);
          }
      
          if (y != 0) {
              double visibleFraction = viewportBounds.getHeight() / layoutBounds.getHeight();
              double range = scrollPane.getVmax() - scrollPane.getVmin();
              double scrollFactor = range * visibleFraction / (1 - visibleFraction);
      
              scrollPane.setVvalue(scrollPane.getVvalue() + y * scrollFactor);
          }
      }

      Specification

      A paragraph is added to the ScrollPane documentation:

      ScrollPane only acts on key presses when it has the focus ({@link #isFocused()} returns {@code true}) and won't respond to
      unconsumed key events that bubble up from a focused child control.

            jhendrikx John Hendrikx
            jhendrikx John Hendrikx
            Kevin Rushforth
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated:
              Resolved: