You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

withKeyboardNavigation.tsx 4.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2021 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import key from 'keymaster';
  21. import * as React from 'react';
  22. import PageActions from '../../components/ui/PageActions';
  23. import { getComponentMeasureUniqueKey } from '../../helpers/component';
  24. import { getWrappedDisplayName } from './utils';
  25. export interface WithKeyboardNavigationProps {
  26. components?: T.ComponentMeasure[];
  27. cycle?: boolean;
  28. isFile?: boolean;
  29. onEndOfList?: () => void;
  30. onGoToParent?: () => void;
  31. onHighlight?: (item: T.ComponentMeasure) => void;
  32. onSelect?: (item: T.ComponentMeasure) => void;
  33. selected?: T.ComponentMeasure;
  34. }
  35. const KEY_SCOPE = 'key_nav';
  36. export default function withKeyboardNavigation<P>(
  37. WrappedComponent: React.ComponentClass<P & Partial<WithKeyboardNavigationProps>>
  38. ) {
  39. return class Wrapper extends React.Component<P & WithKeyboardNavigationProps> {
  40. static displayName = getWrappedDisplayName(WrappedComponent, 'withKeyboardNavigation');
  41. componentDidMount() {
  42. this.attachShortcuts();
  43. }
  44. componentWillUnmount() {
  45. this.detachShortcuts();
  46. }
  47. attachShortcuts = () => {
  48. key.setScope(KEY_SCOPE);
  49. key('up', KEY_SCOPE, () => {
  50. return this.skipIfFile(this.handleHighlightPrevious);
  51. });
  52. key('down', KEY_SCOPE, () => {
  53. return this.skipIfFile(this.handleHighlightNext);
  54. });
  55. key('right,enter', KEY_SCOPE, () => {
  56. return this.skipIfFile(this.handleSelectCurrent);
  57. });
  58. key('left', KEY_SCOPE, () => {
  59. this.handleSelectParent();
  60. return false; // always hijack left
  61. });
  62. };
  63. detachShortcuts = () => {
  64. key.deleteScope(KEY_SCOPE);
  65. };
  66. getCurrentIndex = () => {
  67. const { selected, components = [] } = this.props;
  68. return selected
  69. ? components.findIndex(
  70. component =>
  71. getComponentMeasureUniqueKey(component) === getComponentMeasureUniqueKey(selected)
  72. )
  73. : -1;
  74. };
  75. skipIfFile = (handler: () => void) => {
  76. if (this.props.isFile) {
  77. return true;
  78. } else {
  79. handler();
  80. return false;
  81. }
  82. };
  83. handleHighlightNext = () => {
  84. if (this.props.onHighlight === undefined) {
  85. return;
  86. }
  87. const { components = [], cycle } = this.props;
  88. const index = this.getCurrentIndex();
  89. const first = cycle ? 0 : index;
  90. this.props.onHighlight(
  91. index < components.length - 1 ? components[index + 1] : components[first]
  92. );
  93. if (index + 1 === components.length - 1 && this.props.onEndOfList) {
  94. this.props.onEndOfList();
  95. }
  96. };
  97. handleHighlightPrevious = () => {
  98. if (this.props.onHighlight === undefined) {
  99. return;
  100. }
  101. const { components = [], cycle } = this.props;
  102. const index = this.getCurrentIndex();
  103. const last = cycle ? components.length - 1 : index;
  104. this.props.onHighlight(index > 0 ? components[index - 1] : components[last]);
  105. };
  106. handleSelectCurrent = () => {
  107. if (this.props.onSelect === undefined) {
  108. return;
  109. }
  110. const { selected } = this.props;
  111. if (selected !== undefined) {
  112. this.props.onSelect(selected as T.ComponentMeasure);
  113. }
  114. };
  115. handleSelectNext = () => {
  116. if (this.props.onSelect === undefined) {
  117. return;
  118. }
  119. const { components = [] } = this.props;
  120. const index = this.getCurrentIndex();
  121. if (index !== -1 && index < components.length - 1) {
  122. this.props.onSelect(components[index + 1]);
  123. }
  124. };
  125. handleSelectParent = () => {
  126. if (this.props.onGoToParent !== undefined) {
  127. this.props.onGoToParent();
  128. }
  129. };
  130. handleSelectPrevious = () => {
  131. if (this.props.onSelect === undefined) {
  132. return;
  133. }
  134. const { components = [] } = this.props;
  135. const index = this.getCurrentIndex();
  136. if (components.length && index > 0) {
  137. this.props.onSelect(components[index - 1]);
  138. }
  139. };
  140. render() {
  141. return (
  142. <>
  143. <PageActions showShortcuts={!this.props.isFile} />
  144. <WrappedComponent {...this.props} />
  145. </>
  146. );
  147. }
  148. };
  149. }