quasarframework / quasar

Quasar Framework - Build high-performance VueJS user interfaces in record time
https://quasar.dev
MIT License
26k stars 3.54k forks source link

[v1][Request] Enable keyboard shortcuts on QSelect #4078

Closed satyavh closed 5 years ago

satyavh commented 5 years ago

To speed up entering data through forms, I've implemented a wrapper component around QSelect to enable TAB- and ENTER-select an option, as well as Backspace and Delete a selected option. Like this:

/**
 * Functional HOC that wraps QSelect to add TAB select functionality
 * It basically passes on the context (props, data, attrs, etc) to QSelect
 * and adding some additional listeners to enable Tab select
 */

export default {
  functional: true,
  components: { QSelect, },
  render(createElement, context) {
    // Create a unique ref so this element and its children can be found
    const ref = context.data.ref || Math.random().toString(36).substr(2, 9);

    // Helper to find this HOC in parent tree
    // A functional component has no $vm or 'this'
    // so find its reference through its parent (which is $root)
    const getSelf = (_context) => _context.parent.$refs[ref];

    return createElement('div', {
      attrs: {
        'data-cy': context.data.attrs['data-cy'],  // added data-cy for testing purposes
      },
    },
    [createElement(QSelect, {
      // pass on the complete context
      ...context.data,

      // add native events, QSelect does not emit keydown
      nativeOn: {
        keydown: (e) => {
          // enable tab select
          if (e.key === 'Tab') {
            const $self = getSelf(context);

            // find the child that is focused, which is a QItem component
            const menuItems = $self.$refs.menu.$slots.default;
            const el = menuItems.find((c) => c.data.props.focused);

            if (el) {
              // The QItem does not seem to expose its value through any data prop
              // so find the corresponding selected option manually

              const selectedItem = context.props.options.find((option) => option.label === el.elm.textContent);

              // Emit input event, so that parent $v.model is validated and updated
              if (selectedItem) $self.$emit('input', selectedItem.value);
            }
          }

          // enable backscape and delete remove
          if (['Backspace', 'Delete'].includes(e.key)) {
            getSelf(context).$emit('input', null);
          }
        },

        focusout: () => {
          // emit blur on focusing out
          getSelf(context).$emit('blur');
        },
      },
      ref,
    }),
    ]);
  },
};

specs TAB/ENTER For TAB- and ENTER-select, basically you can use the arrow keys to select an option, and then press TAB/ENTER to select it.

However, due to constant changes to the QSelect component this functionality keeps breaking. As of 1.0.0-beta.20 it does not work anymore because it's impossible to figure out which option (child component) is focused when using arrow keys.

Hence I thought to turn this into a feature request :-)

specs for BACKSPACE/DELETE Pressing BACKSPACE/DELETE clears the selected input, only if an option is selected. As not to interfere with typing and filtering.

Thanks!

qyloxe commented 5 years ago

Well, I would argue that what happens after the specific keystroke should depend on "keystroke strategy" (similar to QTree tick strategy). For example, in one culture pressing "arrow right" means the same as in others pressing "page down", in some localisations/cultures pressing "enter/return" should move to another field in others choosing an option and in others submitting the whole form.

And another condition - there should be global keymapping possibility so one could "map" specific keystrokes on the global level. The keyboard processing is the most important but it should be resolved on the global Quasar level and for every interactive component IMHO. The similar situation is with DnD and "functional verbs" as in "copy", "paste" etc.

In essence developer of the app should choose what "moving to the next item" means (strategy) but user of the app should decide if she wants to activate it by pressing "tab" or "arrow" or "enter".

rstoenescu commented 5 years ago

Hi to both,

Having a global keystroke strategy is NOT possible. You can't expect a component to react to keystrokes. What if you have two or more such components? It creates chaos.

Quasar's strategy is to rely on TAB / SHIFT+TAB natural browser focusing. This will not change. The QSelect keyboard shortcuts have been discussed numerous times. Current behavior: arrows change menu selection then ENTER makes the actual selection, or if no selection, toggles the menu. Then TAB / SHIFT+TAB should change focus to next/previous focusable element on page. Can't just change this TAB / SHIFT+TAB natural browser behavior because there are cases where for example you have closable chips on QSelect's "selected" slot and users should be able to focus on those too (you have to see this otherwise it's hard to explain, but bottom line it would generate a mess with the keystrokes).

Moreover DELETE key removes the last selected value already if input is empty..

qyloxe commented 5 years ago

@rstoenescu @satyavh

Hi to both, Having a global keystroke strategy is NOT possible. You can't expect a component to react to keystrokes. What if you have two or more such components? It creates chaos.

hello,

the solution could be as this:

  1. the interactive components should have implemented only "verbs" which executes desired behaviour (only changes in visual appearance or changes of internal data) - for example "move next", "move previous sibling", "delete item", "paste", "undo", etc. Let me clarify - the components do not observe, or wait, or poll for anything - keystrokes or other global events. They just make available the functionality of executing specific "verbs".

  2. there is a concept of globally "active" interactive component. The globally acquired keystrokes activates specific verbs (by keymapping) ONLY in the actually active interactive component. There is no chaos. If there is no actually active interactive component, those keystrokes could be ignored (or captured by developer in some kind of global event handler). From the end user point of view it is the concept of "focused" element but it is not valid - see later discussion about "focus".

  3. there is a concept of "selected" item, WHEN there is a specific verb activated in the context of specific interactive (active) component, then this verb decides if it has a "selected" item (in the context of this component only) and executing the verb "move to the next item" is possible. Obviously moving to the next selected item if there isn't any would be impossible - BUT - developer would want to override this behaviour (verb) and wants to select in such a situation just a first item (in QTable, QTree, QSelect or even in QEditor).

Discussion: Why the "tab/shift tab" is not quite useful in every interactive app in browser? Well, there are many reasons, but consider this example - the user who wants to quickly enter data only into input fileds with "tab" will change "focus" to every focusable element on the page. Those could be left column tab headers, or navigational buttons, or button helpers, or links - or anything where developer omitted disabling focus or could not do that (links).

Yes, there are utility classes for focus in Quasar, but as I hope, proved here :-) "focusable" is something different than "active" than "selected".

The main problem with the concept of "focusable" are links and buttons. Links have to be focusable and yes, you can argue, that IF every developer would use proper and carefully designed ARIA hints then maybe in the future, browsers would use that as a hint for keystroke navigation/input, but as for now and for any foreseable future that is not the case - they ignore that and just "tab" to another "focused" input, link, button. ARIA hints in form inputs could be used properly (for selective navigation), as for now, only in some screen readers, but it is not our use case, because we want to implement effective way of inputting data without mouse.

Addendum no 1 :

There are two other relevant use cases:

  1. Ctrl+T/N/W. Tab new, tab close etc. in browsers. Those keyboard shortcuts are reserved in some browsers but in some are available to global override by javascript. In the context of QTabs one would want to use Ctrl+W (or Ctrl+F4) to close "active" tab.

  2. Quasar apps in Electron mode. In desktop apps there is no concept of tab/shift tab working the same on every OS. In desktop apps historically the interactive behaviour should be the same as (in this order): in the operating system, its actual localisation, this specific app settings. Restricting keyboard navigation in Electron apps only to Tab/Shift Tab is not in the spirit of "true desktop experience".

Addendum no 2:

  1. Why the concept of differences between "active" (globally) and "selected" (in the component) is important? Because there are now use cases, when one "active" component can have multiple "focusably selected" items. In some editors (VSCode, Notepad++) there is a possibility to have multiple active cursors and write in many places at once (extremely useful sometimes). In collaborative editors (Google Docs, TipTap/ProseMirror) there is a concept of "shared selection" where one user can see, where are placed other users cursors and what other user selections are (in read only mode). It is somewhat different from multiple selection as in QTable because those are multiple "interactive/focusable" selections but in QTable all selected items are treated as one. Well, my point in here is, that the Quasar is built for the future not for what already was.