Manage or block focus on a specific form

21 Views Asked by At

Hope you're good.

I want to know how to manage (or block) focus in a spécifique part of a form.

I have this #sliderContainer which contains 4 forms. If one form is valid, we slide to the other form.

<div #sliderContainer class="relative w-full flex flex-grow overflow-hidden">
        <app-id-form [configForm]="configForm" class="slideItem px-20 py-2"/>

        <app-personal-info-form [configForm]="configForm" class="slideItem"/>

        <app-medical-info-form [configForm]="configForm" class="slideItem px-20 py-2"/>

        <app-emergency-contacts-info-form [configForm]="configForm" class="slideItem px-20 py-2"/>
      </div>

The problem is that even if the form is invalid, if you press the "Tab" key on the keyboard, the focus switches to the input of the next form (which is not the desired behavior).

What I want is to prevent the user from moving on to the next form simply by pressing "Tab", or at least keep the focus on the current form only.

How can I do this?

Additional infos: In the additional information below, you will find out how I check if the current form is valid before I slide to the new one.

//...
// code omited for simplicity purpose
//...
@ViewChild('sliderContainer') sliderContainer!: ElementRef

// When we press next button
protected async nextForm(): Promise<void> {
    const currentStep = await firstValueFrom(this.currentStep$)
    const currentForm = this.configForm.get(currentStep.form)

    if (currentForm!.invalid || currentForm!.pending) return currentForm?.markAllAsTouched()

    this.store.dispatch(configActions.nextForm())
    this.slide().then()
  }

// slide to next form
  async slide() {
    const currentIndex = await firstValueFrom(this.currentStepIndex$)

    this.sliderContainer
      .nativeElement
      .querySelectorAll('.slideItem')
      .forEach((item: ElementRef) => this.renderer.setStyle(item, 'transform', `translateX(${-100 * currentIndex}%)`))
  }
1

There are 1 best solutions below

0
Get Off My Lawn On

You can use a CDK focus trap, and set the focus to a particular trap within the form. The focus trap will trap the cursor within it so tabbing forward/backwards it will not exit the trap.

Note 1: cdkTrapFocus takes a boolean value to enable/disable the trap. By default the trap is enabled.

Note 2: focusFirstTabbableElement() will bring the element into focus if it is hidden from view such as within an overflow or outside the window scroll.

Stackblitz Example

import { Component, QueryList, ViewChildren } from '@angular/core';
import { CommonModule } from '@angular/common';
import { A11yModule, CdkTrapFocus } from '@angular/cdk/a11y';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, A11yModule],
  template: `
    <div class="wrapper">
      @for (i of [1,2,3]; track i) {
        <div>
          <h2>Section {{i}}</h2>
          <div cdkTrapFocus>
            <input type="text" />
            <input type="text" />
            <button (click)="next(i)">Next</button>
          </div>
        </div>
      }
      <div>
        <h2>Done!</h2>
        <div cdkTrapFocus>
          <div tabindex="0"></div>
        </div>
      </div>
    </div>
  `
})
export class App {
  @ViewChildren(CdkTrapFocus)
  traps!: QueryList<CdkTrapFocus>;

  next(id: number) {
    this.traps.get(id)?.focusTrap.focusFirstTabbableElement();
  }
}

Here is the related SCSS

* {
  box-sizing: border-box;
}
.wrapper {
  width: 200px;
  overflow: hidden;
  display: flex;
  flex-direction: row;

  & > div {
    width: 200px;
    min-width: 200px;
    padding: 5px;
    & > div {
      display: flex;
      flex-direction: column;
      gap: 5px;
    }
  }

  input,
  button {
    width: 100%;
    display: inline-block;
    border: solid 1px black;
    &:focus-within {
      border-radius: 2px;
      outline: solid 2px royalblue;
      outline-offset: 2px;
    }
  }
}