0.2.0 • Published 3 years ago

ember-focus v0.2.0

Weekly downloads
3
License
MIT
Repository
github
Last release
3 years ago

ember-focus

NPM Version Ember Observer Score

Ember implementation to handle focus: management, trapping, throwing, catching, releasing, guiding, announcing

Compatibility

  • Ember.js v3.16 or above
  • Ember CLI v2.13 or above
  • Node.js v10 or above

Installation

ember install ember-focus

Usage

ember-focus provides modifiers to be used in your components and applications.

focus service

NOTE: These modifiers leverage a focus-service to communicate between themselves. However, the service is a private API and its internals are implementation details which may change at any time. Do not use it directly. Instead, only manage focus via the modifiers.

focus modifier

<FancyInput
  {{focus when=this.shouldFocus onSelector='input'}}
/>

trap-focus modifier

<FancyTextArea {{trap-focus}}></FancyTextArea>

default focus

NOTE: These modifiers DO NOT leverage the focus-service to communicate between themselves. Instead, browser default focus calls are made and the service has no awareness of previous focus history

trap-arrows

<MyAwesomeContainer {{trap-arrows}}>
  <button ...>
  <button ...>
</MyAwesomeContainer>

API

Modifier: focus

Set focus on an element.

By default, this focuses the element the modifier is installed on as soon as it is rendered.

When the element is destroyed, the modifier returns focus to whatever had it prior to the modified-element taking it.

<label>
  Name:
  <input
    value={{this.value}}
    id="name"
    {{ember-focus$focus}}
    {{on "input" this.updateValue}}
  />
</label>

However, this also supports a variety of alternatives:

  • You can specify when to focus the element:

    <label>
      Name:
      <input
        value={{this.value}}
        id="name"
        {{ember-focus$focus
          when=(eq this.inputToFocus "name")}}
        {{on "input" this.updateValue "name"}}
      />
    </label>

    As with element destruction, focus returns to the previous element any time when is set to false.

  • You can specify a selector that is a child of the element, if working with a component that accepts modifiers and which provides information about its internals to make this kind of thing safe:

    <FancyInput
      @label="Name"
      @value={{this.value}}
      @id="name"
      {{ember-focus$focus
        onSelector="input"}}
      {{on "input" this.updateValue "name"}}
    />
  • You can specify to set the cursor at the end of any text which is already populating an input:

    <label>
      Name:
      <input
        value={{this.value}}
        id="name"
        {{ember-focus$focus
          withCursorAtEnd=true}}
        {{on "input" this.updateValue "name"}}
      />
    </label>
  • You can specify an alternative element to return focus when the targeted element is destroyed or when is set to false:

    <label>
      Name:
      <input
        value={{this.value}}
        id="name"
        {{ember-focus$focus
          returnTo=this.anotherSelectorValue}}
        {{on "input" this.updateValue "name"}}
      />
    </label>
  • You can prevent auto-scrolling to the element:

    <label>
      Name:
      <input
        value={{this.value}}
        id="name"
        {{ember-focus$focus
          preventScroll=true}}
        {{on "input" this.updateValue "name"}}
      />
    </label>
  • You can optionally disable use of the focus-manager service and forget where focus was

    <label>
      Name:
      <input
        value={{this.value}}
        id="name"
        {{ember-focus$focus
          forget=true}}
        {{on "input" this.updateValue "name"}}
      />
    </label>
  • You can combine any or all of these features:

    <FancyInput
      @label="Name"
      @value={{this.value}}
      @id="name"
      {{ember-focus$focus
        when=(eq this.inputToFocus "name")
        onSelector="input"
        withCursorAtEnd=true
        returnTo=this.anotherSelectorValue
        preventScroll=true
        forget=true
      }}
      {{on "input" this.updateValue "name"}}
    />

Arguments

NameTypeRequiredDefaultDescription
whenbooleanoptionaltruewhether the element should focus. Default true.
onSelectorstringoptionalundefinedan optional query select to target an inner child of the element
returnTostringoptionalundefinedan optional query select to return focus to instead of the default
withCursorAtEndbooleanoptionalfalseonly for input/textarea element: whether we should put cursor at end of input after focus
preventScrollbooleanoptionalfalsebool prop for focus call
forgetbooleanoptionalfalseif true, do not use the focus-manager service and forget where focus came from

focus: example

<FancyInput
  @label="Name"
  @value={{this.value}}
  @id="name"
  {{focus
    when=this.shouldFocus
    onSelector='input'
    returnTo='nav'
    preventScroll=true
    withCursorAtEnd=true
    forget=true}}
/>

Modifier: trap-focus

Traps focus within an element.

trap-focus: example

<FancyTextArea {{trap-focus}}></FancyTextArea>

Modifier: trap-arrows

Traps arrow navigation within a given element, leaving tab to default behavior. Using arrow keys to navigate will wrap within the element, and skip disabled elements, appropriately.

trap-arrows: example

With no setup, up/down arrow keys will can be used within a given element:

<MyAwesomeContainer {{trap-arrows}}>
  <button type="button">Button A</button>
  <button type="button">Button B</button>
</MyAwesomeContainer>

Alternatively, you can set it up for horizontal use, and left/right arrow keys will now respond to focusing the previous/next element within the given element.

<MyAwesomeContainer {{trap-arrows orientation='horizontal'}}>
  <button type="button">Button A</button>
  <button type="button">Button B</button>
</MyAwesomeContainer>

By default, all focusable elements (a hefty list of elements that are non-disabled, with a tabindex > -1) within the attached element are cycled through.

If, for any reason you need to override the default list, you can pass in an optional position-parameter as the selector:

<MyAwesomeContainer {{trap-arrows '[role=tab]'}}>
  <button type="button">Button A</button>
  <button type="button" role="tab">Button B</button>
  <button type="button">Button C</button>
  <button type="button" role="tab">Button D</button>
</MyAwesomeContainer>

More Examples

Combining the focus modifier with the trap-focus and handle-arrow-navigation-focus modifiers.

<FancyTextArea
  {{trap-focus}}
  {{focus when=this.shouldFocus}}
  ></FancyTextArea>
<MenuContainer
  {{handle-arrow-navigation-focus}}
  {{focus when=this.shouldFocus}}
  ></MenuContainer>

Accessibility Considerations

  • Non-natively focusable elements need a tabindex If you want to set focus on an element which is not naturally focusable, you will need to make the element focusable using tabindex (eg, tabindex="-1").
  • Be predictable; don't be unexpected Focus should only ever be explicitly requested and/or managed through direct user action. It should be discernable and predictable.
  • Trapping focus requires an escape route If you implement some component to {{trap-focus}}, don't forget to create a mechanism to bail out! Often times the Escape key or some sort of Cancel button. A common example is the modal dialog pattern.

Contributing

See the Contributing guide for details.

License

This project is licensed under the MIT License.