Search code examples
htmlangulartypescriptfrontendfocus

Is there any way to render focus on ng-template modal using keyboard arrow key?


I used one directive and service for gaining focus and that is working on all pages when pressing the arrow keys but on ng-template modal the focus is not gathering and am getting the find index value -1.Please refer this link Link for complete code to see the full code.The solution provided in that link is working on everywhere except the ng-template modal.Please refer with some ideas to solve this issues.

   <ng-template #content let-modal>
     <div class="modal-header">
     <h3 class="modal-title">Accept Offer</h3>
     </div>
     <div class="modal-body">
     <p>Do you really want to accept the offer?</p>
     </div>
     <div class="modal-footer>
     <button type="submit" class="btn btn-primary tab" (click)="onAcceptOffer()" arrow-div>Submit</button>
        <button type="button" class="btn btn-secondary tab" (click)="modal.dismiss('Crossclick');
       isClicked=false" arrow-div>Cancel</button></div>
     </ng-template>

Solution

  • Update 2022-08-11 Control input Select (I change the function onArrowDown)

    We can improve the code of the link using a directive instead of use fromEvent in main component.

    I want that keydown can be listend from document or from another div. Futhermore, we need control if the element is an Input or a select to change a bit how the arrow work

    So we can make a directive like

    export enum Key {
      Tab = 9,
      Enter = 13,
      Escape = 27,
      Space = 32,
      PageUp = 33,
      PageDown = 34,
      End = 35,
      Home = 36,
      ArrowLeft = 37,
      ArrowUp = 38,
      ArrowRight = 39,
      ArrowDown = 40,
    }
    
    @Directive({
      selector: '[div-group-arrow]',
    })
    export class DivGroupArrowDirective implements OnInit, OnDestroy {
      @ContentChildren(ControlArrowDirective, { descendants: true })
      items: QueryList<ControlArrowDirective>;
      active: boolean = true;
      subscription: Subscription;
    
      @Input() step = 1;
      @Input() focus = true;
      @Input() main = false;
      constructor(private elementRef: ElementRef) {}
      ngOnInit() {
        this.subscription = fromEvent(
          this.main ? document : this.elementRef.nativeElement,
          'keydown'
        )
          .pipe(
            filter(
              (event: any) =>
                this.active &&
                event.which >= Key.ArrowLeft &&
                event.which <= Key.ArrowDown
            )
          )
          .subscribe((event) => {
            this.onArrowDown(event);
          });
      }
      ngOnDestroy() {
        this.subscription && this.subscription.unsubscribe();
      }
      onArrowDown(event: any) {
        const focused = this.items.find(
          (x) => x.elementRef.nativeElement == document.activeElement
        );
        if (!focused) {
          this.items.first.elementRef.nativeElement.focus();
          return;
        }
        let index = this.items.reduce((a, b, i) => (b == focused ? i : a), 0);
    
        const htmlElement = focused.elementRef.nativeElement;
        const isInput = htmlElement.tagName == 'INPUT';
        const isSelect=htmlElement.tagName == 'SELECT';
        const oldIndex = index;
        switch (event.which) {
          case Key.ArrowLeft:
            if ((isInput && !htmlElement.selectionStart) || isSelect)event.preventDefault();
            index += !isInput || !htmlElement.selectionStart ? -1 : 0;
            break;
          case Key.ArrowRight:
            if ((isInput && htmlElement.selectionEnd == htmlElement.value.length) || isSelect)
              event.preventDefault();
            index +=
              !isInput || htmlElement.selectionEnd == htmlElement.value.length
                ? 1
                : 0;
            break;
          case Key.ArrowUp:
            if (!isSelect) {
              index -= this.step;
              event.preventDefault();
            }
            break;
          case Key.ArrowDown:
            if (!isSelect) {
              index += this.step;
              event.preventDefault();
            }
            break;
        }
        if (index >= 0 && index < this.items.length && index != oldIndex) {
          const next = this.items.find((_, i) => i == index).elementRef
            .nativeElement;
          if (next.tagName == 'INPUT') {
            next.selectionStart = 0;
            next.selectionEnd = this.focus ? next.value.length : 0;
          }
          next.focus();
        }
      }
    
    }
    

    See that we have a property "active" to help us "stop" the listener

    The control-arrow is simple

    @Directive({
      selector: '[control-arrow]'
    })
    export class ControlArrowDirective {
    
      constructor(public elementRef:ElementRef) { }
    
    }
    

    We can then use, e.g. (I use ngb-bootstrap modal but we can use the same using material or whatever

    <!--see that in main.html, we use [main]="true"-->
    <div class="container mt-3" div-group-arrow [main]="true" [step]="2">
      <input control-arrow class="me-2 my-2" />
      <input control-arrow class="me-2 my-2"/><br />
      <input control-arrow class="me-2"/>
      <input control-arrow class="me-2 mb-3"/>
    
      <button class="btn btn-lg btn-outline-primary" (click)="open(content, null)">
        Launch demo modal
      </button>
    </div>
    
    <ng-template #content let-modal>
      <!---we enclose all in a div with the directive "div-group-arrow"-->
      <div div-group-arrow>
        <div class="modal-header">
          ...
        </div>
        <div class="modal-body">
          ...
        </div>
        <div class="modal-footer">
          <button
            control-arrow
            type="button"
            class="btn btn-outline-dark"
            (click)="modal.close('Save click')"
          >
            Save
          </button>
    
          <button
            control-arrow
            type="button"
            class="btn btn-outline-dark"
            (click)="modal.dismiss('Cancel click')"
          >
            Cancel
          </button>
        </div>
      </div>
    </ng-template>
    

    The stackblitz

    Update Really I don't like the property "active". We can avoid simply change the filter

    .pipe(
        filter(
          (event: any) =>
            event.which >= Key.ArrowLeft &&
            event.which <= Key.ArrowDown &&
            ((document.activeElement.tagName=='BODY' && this.main)
                  ||this.elementRef.nativeElement.contains(document.activeElement))
        )
      )