【问题标题】:Navigate the UI using only keyboard仅使用键盘导航 UI
【发布时间】:2013-06-30 07:02:33
【问题描述】:

我正在尝试仅使用键盘浏览记录列表。当页面加载时,默认的“焦点”应该在第一条记录上,当用户点击键盘上的向下箭头时,需要关注下一条记录。当用户单击向上箭头时,应聚焦前一条记录。当用户单击 Enter 按钮时,它应该将他们带到该记录的详细信息页面。

Here's what I have so far on Plunkr.

这似乎在 AngularJS 1.1.5(不稳定)中得到支持,我们不能在生产中使用。我目前使用的是 1.0.7。我希望做这样的事情 - 应该在文档级别处理密钥。当用户按下某个键时,代码应在允许的键数组中查找。如果找到匹配项(例如向下键代码),它应该将焦点(应用 .highlight css)移动到下一个元素。当按下 enter 时,它应该获取 .highlight css 的记录并获取记录 id 以进行进一步处理。

谢谢!

【问题讨论】:

    标签: angularjs navigation keypress angular-ui


    【解决方案1】:

    以下是您可以选择执行的示例: http://plnkr.co/edit/XRGPYCk6auOxmylMe0Uu?p=preview

    <body key-trap>
      <div ng-controller="testCtrl">
        <li ng-repeat="record in records">
          <div class="record"
               ng-class="{'record-highlight': record.navIndex == focu sIndex}">
            {{ record.name }}
          </div>
        </li>
      </div>
    </body>
    

    这是我能想到的最简单的方法。 它将指令keyTrap 绑定到捕获keydown 事件的body$broadcast 给子作用域的消息。 元素持有者范围将捕获消息并简单地增加或减少 如果点击enter,则使用focusIndex 或触发open 函数。

    编辑

    http://plnkr.co/edit/rwUDTtkQkaQ0dkIFflcy?p=preview

    现在支持有序/过滤列表。

    事件处理部分没有改变,但现在使用$index 和过滤列表缓存 技术结合起来跟踪哪个项目得到关注。

    【讨论】:

    • 这是一个不错的方法,但是当条目被排序时它不起作用(record in records | orderBy: '-name')。你也有解决方案吗? (不只是针对这种情况,而是更通用的情况)
    • 感谢您的反馈。接受更困难的用例挑战总是很有趣和有把握的。我添加了支持排序/过滤列表的附加代码。
    • 谢谢!您的解决方案非常鼓舞人心。
    • 这使得单击所有键不会执行默认行为。例如 f5 不刷新。无论如何要解决这个问题?
    • @AryehArmon 我用 chrome 测试了我的 plunker 页面,F5 确实刷新了。我的理解是,如果您运行event.preventDefault(),则默认键绑定(例如 F5)将被取消,否则 F5 应该可以工作。我建议你加载 jQuery 而不是使用 jqLit​​e(它是 Angular 的一部分)。
    【解决方案2】:

    目前提供的所有解决方案都有一个共同问题。这些指令不可重用,它们需要了解在控制器提供的父 $scope 中创建的变量。这意味着如果您想在不同的视图中使用相同的指令,您需要重新实现您在前一个控制器中所做的所有事情,并确保您使用相同的变量名称,因为指令基本上具有硬编码的 $scope 变量名称在他们之中。你肯定不能在同一个父作用域内使用同一个指令两次。

    解决此问题的方法是在指令中使用隔离范围。通过这样做,您可以通过对父范围所需的项进行一般参数化,使指令可重用,而不管父 $scope 是什么。

    在我的解决方案中,控制器唯一需要做的就是提供一个 selectedIndex 变量,该指令用于跟踪当前选择了表中的哪一行。我本可以将此变量的责任与指令隔离开来,但通过让控制器提供变量,它允许您在指令之外操作表中当前选定的行。例如,您可以在控制器中实现“单击选择行”,同时仍使用箭头键在指令中导航。

    指令:

    angular
        .module('myApp')
        .directive('cdArrowTable', cdArrowTable);
        .directive('cdArrowRow', cdArrowRow);
    
    function cdArrowTable() {
        return {
            restrict:'A',
            scope: {
                collection: '=cdArrowTable',
                selectedIndex: '=selectedIndex',
                onEnter: '&onEnter'
            },
            link: function(scope, element, attrs, ctrl) {
                // Ensure the selectedIndex doesn't fall outside the collection
                scope.$watch('collection.length', function(newValue, oldValue) {
                    if (scope.selectedIndex > newValue - 1) {
                        scope.selectedIndex = newValue - 1;
                    } else if (oldValue <= 0) {
                        scope.selectedIndex = 0;
                    }
                });
    
                element.bind('keydown', function(e) {
                    if (e.keyCode == 38) {  // Up Arrow
                        if (scope.selectedIndex == 0) {
                            return;
                        }
                        scope.selectedIndex--;
                        e.preventDefault();
                    } else if (e.keyCode == 40) {  // Down Arrow
                        if (scope.selectedIndex == scope.collection.length - 1) {
                            return;
                        }
                        scope.selectedIndex++;
                        e.preventDefault();
                    } else if (e.keyCode == 13) {  // Enter
                        if (scope.selectedIndex >= 0) {
                            scope.collection[scope.selectedIndex].wasHit = true;
                            scope.onEnter({row: scope.collection[scope.selectedIndex]});
                        }
                        e.preventDefault();
                    }
    
                    scope.$apply();
                });
            }
        };
    }
    
    function cdArrowRow($timeout) {
        return {
            restrict: 'A',
            scope: {
                row: '=cdArrowRow',
                selectedIndex: '=selectedIndex',
                rowIndex: '=rowIndex',
                selectedClass: '=selectedClass',
                enterClass: '=enterClass',
                enterDuration: '=enterDuration'  // milliseconds
            },
            link: function(scope, element, attrs, ctr) {
                // Apply provided CSS class to row for provided duration
                scope.$watch('row.wasHit', function(newValue) {
                    if (newValue === true) {
                        element.addClass(scope.enterClass);
                        $timeout(function() { scope.row.wasHit = false;}, scope.enterDuration);
                    } else {
                        element.removeClass(scope.enterClass);
                    }
                });
    
                // Apply/remove provided CSS class to the row if it is the selected row.
                scope.$watch('selectedIndex', function(newValue, oldValue) {
                    if (newValue === scope.rowIndex) {
                        element.addClass(scope.selectedClass);
                    } else if (oldValue === scope.rowIndex) {
                        element.removeClass(scope.selectedClass);
                    }
                });
    
                // Handles applying/removing selected CSS class when the collection data is filtered.
                scope.$watch('rowIndex', function(newValue, oldValue) {
                    if (newValue === scope.selectedIndex) {
                        element.addClass(scope.selectedClass);
                    } else if (oldValue === scope.selectedIndex) {
                        element.removeClass(scope.selectedClass);
                    }
                });
            }
        }
    }
    

    该指令不仅允许您使用箭头键导航表格,还允许您将回调方法绑定到 Enter 键。这样当按下 enter 键时,当前选择的行将作为参数包含在使用指令 (onEnter) 注册的回调方法中。

    作为一个额外的好处,您还可以将 CSS 类和持续时间传递给 cdArrowRow 指令,这样当在选定行上按下回车键时,传入的 CSS 类将应用于该行元素,然后将其删除在传入的持续时间(以毫秒为单位)之后。这基本上可以让你做一些事情,比如当按下回车键时让行闪烁不同的颜色。

    查看使用情况:

    <table cd-arrow-table="displayedCollection"
           selected-index="selectedIndex"
           on-enter="addToDB(row)">
        <thead>
            <tr>
                <th>First Name</th>
                <th>Last Name</th>
            </tr>
        </thead>
        <tbody>
            <tr ng-repeat="row in displayedCollection" 
                cd-arrow-row="row" 
                selected-index="selectedIndex" 
                row-index="$index" 
                selected-class="'mySelcetedClass'" 
                enter-class="'myEnterClass'" 
                enter-duration="150"
            >
                <td>{{row.firstName}}</td>
                <td>{{row.lastName}}</td>
            </tr>
        </tbody>
    </table>
    

    控制器:

    angular
        .module('myApp')
        .controller('MyController', myController);
    
        function myController($scope) {
            $scope.selectedIndex = 0;
            $scope.displayedCollection = [
                {firstName:"John", lastName: "Smith"},
                {firstName:"Jane", lastName: "Doe"}
            ];
            $scope.addToDB;
    
            function addToDB(item) {
                // Do stuff with the row data
            }
        }
    

    【讨论】:

      【解决方案3】:

      这是我曾经为类似问题构建的以下指令。 该指令侦听键盘事件并更改行选择。

      此链接对如何构建它有完整的说明。 Change row selection using arrows

      这里是指令

      foodApp.directive('arrowSelector',['$document',function($document){
      return{
          restrict:'A',
          link:function(scope,elem,attrs,ctrl){
              var elemFocus = false;             
              elem.on('mouseenter',function(){
                  elemFocus = true;
              });
              elem.on('mouseleave',function(){
                  elemFocus = false;
              });
              $document.bind('keydown',function(e){
                  if(elemFocus){
                      if(e.keyCode == 38){
                          console.log(scope.selectedRow);
                          if(scope.selectedRow == 0){
                              return;
                          }
                          scope.selectedRow--;
                          scope.$apply();
                          e.preventDefault();
                      }
                      if(e.keyCode == 40){
                          if(scope.selectedRow == scope.foodItems.length - 1){
                              return;
                          }
                          scope.selectedRow++;
                          scope.$apply();
                          e.preventDefault();
                      }
                  }
              });
          }
      };
      

      }]);

      <table class="table table-bordered" arrow-selector>....</table>
      

      还有你的中继器

           <tr ng-repeat="item in foodItems" ng-class="{'selected':$index == selectedRow}">
      

      【讨论】:

        【解决方案4】:

        我有一个类似的要求来支持使用箭头键的 UI 导航。我最终想到的是封装在 AngularJS 指令中的 DOM 的 keydown 事件处理程序:

        HTML:

        <ul ng-controller="MainCtrl">
            <li ng-repeat="record in records">
                <div focusable tag="record" on-key="onKeyPressed" class="record">
                    {{ record.name }}
                </div>
            </li>
        </ul>
        

        CSS:

        .record {
            color: #000;
            background-color: #fff;
        }
        .record:focus {
            color: #fff;
            background-color: #000;
            outline: none;
        }
        

        JS:

        module.directive('focusable', function () {
            return {
                restrict: 'A',
                link: function (scope, element, attrs) {
                    element.attr('tabindex', '-1'); // make it focusable
        
                    var tag = attrs.tag ? scope.$eval(attrs.tag) : undefined; // get payload if defined
                    var onKeyHandler = attrs.onKey ? scope.$eval(attrs.onKey) : undefined;
        
                    element.bind('keydown', function (event) {
                        var target = event.target;
                        var key = event.which;
        
                        if (isArrowKey(key)) {
                            var nextFocused = getNextElement(key); // determine next element that should get focused
                            if (nextFocused) {
                                nextFocused.focus();
                                event.preventDefault();
                                event.stopPropagation();
                            }
                        }
                        else if (onKeyHandler) {
                            var keyHandled = scope.$apply(function () {
                                return onKeyHandler.call(target, key, tag);
                            });
        
                            if (keyHandled) {
                                event.preventDefault();
                                event.stopPropagation();
                            }
                        }
                    });
                }
            };
        });
        
        function MainCtrl ($scope, $element) {
            $scope.onKeyPressed = function (key, record) {
                if (isSelectionKey(key)) {
                    process(record);
                    return true;
                }
                return false;
            };
        
            $element.children[0].focus(); // focus first record
        }
        

        【讨论】:

        • @SidBhalke, isArrowKey() 确定是否按下了箭头键,即key &gt;= 37 &amp;&amp; key &lt;= 40。函数getNextElement() 根据键方向和导航逻辑返回要聚焦的元素。它可能是硬编码的 switch case,或者通常使用其getBoundingClientRect() 搜索最近的元素。
        • stackoverflow.com/questions/27956752/… ...你能看看这个链接吗,
        【解决方案5】:

        您可以创建一个表格导航服务来跟踪当前行并公开导航方法以修改当前行的值并将焦点设置到该行。

        然后您需要做的就是创建一个键绑定指令,您可以在其中跟踪按键事件并在按键向上或按键向下时从表格导航服务触发公开的方法。

        我使用控制器通过一个名为“keyDefinitions”的配置对象将服务方法链接到键绑定指令。

        您可以扩展 keyDefinitions 以包含 Enter 键(代码:13)并通过服务属性“tableNavigationService.currentRow”或“$scope.data”连接到选定的 $index 值,然后将其作为参数传递给您自己的自定义 submit() 函数。

        我希望这对某人有帮助。

        我已在以下 plunker 位置发布了对此问题的解决方案:

        Keyboard Navigation Service Demo

        HTML:

        <div key-watch>
          <table st-table="rowCollection" id="tableId" class="table table-striped">
            <thead>
              <tr>
                <th st-sort="firstName">first name</th>
                <th st-sort="lastName">last name</th>
                <th st-sort="birthDate">birth date</th>
                <th st-sort="balance" st-skip-natural="true">balance</th>
                <th>email</th>
              </tr>
            </thead>
            <tbody>
              <!-- ADD CONDITIONAL STYLING WITH ng-class TO ASSIGN THE selected CLASS TO THE ACTIVE ROW -->
              <tr ng-repeat="row in rowCollection track by $index" tabindex="{{$index + 1}}" ng-class="{'selected': activeRowIn($index)}">
                <td>{{row.firstName | uppercase}}</td>
                <td>{{row.lastName}}</td>
                <td>{{row.birthDate | date}}</td>
                <td>{{row.balance | currency}}</td>
                <td>
                  <a ng-href="mailto:{{row.email}}">email</a>
                </td>
              </tr>
            </tbody>
          </table>
        </div>
        

        控制器:

          app.controller('navigationDemoController', [
            '$scope',
            'tableNavigationService',
            navigationDemoController
          ]);
        
          function navigationDemoController($scope, tableNavigationService) {
            $scope.data = tableNavigationService.currentRow;
        
            $scope.keyDefinitions = {
              'UP': navigateUp,
              'DOWN': navigateDown
            }
        
            $scope.rowCollection = [
              {
                firstName: 'Chris',
                lastName: 'Oliver',
                birthDate: '1980-01-01',
                balance: 100,
                email: 'chris@email.com'
              },
              {
                firstName: 'John',
                lastName: 'Smith',
                birthDate: '1976-05-25',
                balance: 100,
                email: 'chris@email.com'
              },
              {
                firstName: 'Eric',
                lastName: 'Beatson',
                birthDate: '1990-06-11',
                balance: 100,
                email: 'chris@email.com'
              },
              {
                firstName: 'Mike',
                lastName: 'Davids',
                birthDate: '1968-12-14',
                balance: 100,
                email: 'chris@email.com'
              }
            ];
        
            $scope.activeRowIn = function(index) {
              return index === tableNavigationService.currentRow;
            };
        
            function navigateUp() {
              tableNavigationService.navigateUp();
            };
        
            function navigateDown() {
              tableNavigationService.navigateDown();
            };
        
            function init() {
              tableNavigationService.setRow(0);
            };
        
            init();
          };
        })();
        

        服务和指令:

        (function () {
          'use strict';
        
          var app = angular.module('tableNavigation', []);
        
          app.service('tableNavigationService', [
            '$document',
            tableNavigationService
          ]);
          app.directive('keyWatch', [
            '$document',
            keyWatch
          ]);
        
          // TABLE NAVIGATION SERVICE FOR NAVIGATING UP AND DOWN THE TABLE
          function tableNavigationService($document) {
            var service = {};
        
            // Your current selected row
            service.currentRow = 0;
            service.table = 'tableId';
            service.tableRows = $document[0].getElementById(service.table).getElementsByTagName('tbody')[0].getElementsByTagName('tr');
        
            // Exposed method for navigating up
            service.navigateUp = function () {
                if (service.currentRow) {
                    var index = service.currentRow - 1;
        
                    service.setRow(index);
                }
            };
        
            // Exposed method for navigating down
            service.navigateDown = function () {
                var index = service.currentRow + 1;
        
                if (index === service.tableRows.length) return;
        
                service.setRow(index);
            };
        
            // Expose a method for altering the current row and focus on demand
            service.setRow = function (i) {
                service.currentRow = i;
                scrollRow(i);
            }
        
            // Set focus to the active table row if it exists
            function scrollRow(index) {
                if (service.tableRows[index]) {
                    service.tableRows[index].focus();
                }
            };
        
            return service;
          };
        
          // KEY WATCH DIRECTIVE TO MONITOR KEY DOWN EVENTS
          function keyWatch($document) {
            return {
              restrict: 'A',
              link: function(scope) {
                $document.unbind('keydown').bind('keydown', function(event) {
                  var keyDefinitions = scope.keyDefinitions;
                  var key = '';
        
                  var keys = {
                      UP: 38,
                      DOWN: 40,
                  };
        
                  if (event && keyDefinitions) {
        
                    for (var k in keys) {
                      if (keys.hasOwnProperty(k) && keys[k] === event.keyCode) {
                          key = k;
                      }
                    }
        
                    if (!key) return;
        
                    var navigationFunction = keyDefinitions[key];
        
                    if (!navigationFunction) {
                      console.log('Undefined key: ' + key);
                      return;
                    }
        
                      event.preventDefault();
                        scope.$apply(navigationFunction());
                        return;
                  }
                  return;
                });
              }
            }
          }
        })();
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-12-07
          • 2011-03-29
          • 1970-01-01
          • 1970-01-01
          • 2017-06-11
          相关资源
          最近更新 更多