Advertisement

#23 Finish Animating the Control Groups

In this article we will finish the process of animating our control groups. This will require us to define and then implement the control flow that we want when the user clicks on the group buttons or uses the keyboard to navigate our interface.

You can view a completed example of the authenticator component here.

Simplifying hiding, showing and more

  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.ts
          • control-group.ts
          • control-pair.ts

Now that we are able to animate the hiding of our control groups we also need to apply the hidden-group class to them once the animation has completed. To do this we just need to change our hideGroupToLeftOrRight method in the control-group.ts file so that it returns the animation and set the onfinish function for it in both the hideGroupToLeft and hideGroupToRight methods to call the hideGroup method (a). While we are making changes to the hide methods we can also make some changes to our show methods to make them easier to use. The first thing we can do is to call the showGroup method before running an animation. We can also return the animation from the showGroupFromLeftOrRight method so that we can focus the input once the animation is finished. Next up is modifying our control-pair.ts class so that when we call the showGroup method it runs the appropriate animation for us. We will do this by simple comparing the control that we find using the id that is passed in to the left and right controls of the pair. If it is equal to the left then we will show the left control from the left and hide the right control to the right and vise versa for showing the right control (b). With those changes made we can simplify the onClickGroupButton method located within the authenticator.component.ts file (c).

control-group.ts (1)

...
export class ControlGroup {
    ...
    public hideGroupToLeft = () => {
        ...
        const animation = this.hideGroupToLeftOrRight(...);
        animation.onfinish = () => {
            this.hideGroup();
        };
    }

    public hideGroupToRight = () => {
        ...
        const animation = this.hideGroupToLeftOrRight(...);
        animation.onfinish = () => {
            this.hideGroup();
        };
    }
    ...
    public showGroupFromLeft = () => {
        this.showGroup();
        ...
        const animation = this.showGroupFromLeftOrRight(...);
        animation.onfinish = () => {
            this.focus();
        };
    }

    public showGroupFromRight = () => {
        this.showGroup();
        ...
        const animation = this.showGroupFromLeftOrRight(...);
        animation.onfinish = () => {
            this.focus();
        };
    }
    ...
    private hideGroupToLeftOrRight = (...) => {
        return this.groupDOM.animate(...);
    }

    private showGroupFromLeftOrRight = (...) => {
        return this.groupDOM.animate(...);
    }
}
(a) Modifying our control group so that the appropriate display class is applied depending on whether the control is being shown or hidden. If it is being shown we will also automatically focus the input.

control-pair.ts (1)

...
export class ControlPair {
    ...
    public showGroup = (id: string) => {
        const group = this.find(id);
        if (group === this._left) {
            group.showGroupFromLeft();
            this._right.hideGroupToRight();
        } else {
            group.showGroupFromRight();
            this._left.hideGroupToLeft();
        }
    }
    ...
}
(b) We can use the control pair to determine which controls need to be shown or hidden and in what direction the animation should run.

authenticator.component.ts (1)

...
export class AuthenticatorComponent ... {
    ...
    public onClickGroupButton = (key: string) => {
        switch(key) {
            case this.controlEmailKey:
                this._controlPairCollection.showGroup(this.controlEmailConfirmGroupId);
                break;
            case this.controlEmailConfirmKey:
                this._controlPairCollection.showGroup(this.controlEmailGroupId);
                break;
            case this.controlPasswordKey:
                this._controlPairCollection.showGroup(this.controlPasswordConfirmGroupId);
                break;
            case this.controlPasswordConfirmKey:
                this._controlPairCollection.showGroup(this.controlPasswordGroupId);
                break;
        }
    }
    ...
 }
(c) Now we no longer need to show/hide individual control groups ourselves.

Which control is next or previous?

The next thing that we will tackle is being able to determine which control should be displayed independent of whether or not the user clicks the button on uses the keyboard. Image (d) shows the flow between controls if the user is logging in and image (e) shows the flow if the user is registering. In both images the starting point is the username control at the top and ending point is the buttons at the bottom. If they are using the keyboard to navigate the path that should be followed if they are holding down the alt key should be the same except in reverse.

The flow from the username to buttons if the user is logging in.
(d) The flow from the username to buttons if the user is logging in.
The flow from the username to buttons if the user is registering.
(e) The flow from the username to buttons if the user is registering.
  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.ts
          • control-pair.ts

Since we need to handle both the clicking of the input group button and keyboard navigation what we need to do then is create a couple of helper methods that we can use to determine the next and previous controls (f). The next issue we want to deal with is if the control we are navigating to is already visible we do not want the show animation running. To prevent it from running we are simple going to introduce a field that will keep track of which control is visible in the control pair (g).

authenticator.component.ts (2)

...
export class AuthenticatorComponent ... {
    ...
    public onClickGroupButton = (key: string) => {
        this.nextControl(key);
    }
    ...
    private nextControl = (key: string) => {
        switch (key) {
            case this.controlUsernameKey:
                this._controlPairCollection.showGroup(this.controlPasswordGroupId);
                break;
            case this.controlPasswordKey:
                if (this.isLogin) {
                    this._controlPairCollection.focus(this.controlRememberMeGroupId);
                } else if (this.isRegister) {
                    this._controlPairCollection.showGroup(this.controlPasswordConfirmGroupId);
                }
                break;
            case this.controlPasswordConfirmKey:
                this._controlPairCollection.showGroup(this.controlEmailGroupId);
                break;
            case this.controlEmailKey:
                this._controlPairCollection.showGroup(this.controlEmailConfirmGroupId);
                break;
            case this.controlEmailConfirmKey:
                if (this.authForm.valid) {
                    this.buttonSendDOM.focus();
                } else {
                    this.buttonResetDOM.focus();
                }
                break;
            case this.controlRememberMeKey:
                if (this.authForm.valid) {
                    this.buttonSendDOM.focus();
                } else {
                    this.buttonResetDOM.focus();
                }
                break;
            }
    }

    private previousControl = (key: string) => {
        switch (key) {
            case this.controlPasswordKey:
                this._controlPairCollection.showGroup(this.controlUsernameGroupId);
                break;
            case this.controlPasswordConfirmKey:
                this._controlPairCollection.showGroup(this.controlPasswordGroupId);
                break;
            case this.controlEmailKey:
                this._controlPairCollection.showGroup(this.controlPasswordConfirmGroupId);
                break;
            case this.controlEmailConfirmKey:
                this._controlPairCollection.showGroup(this.controlEmailGroupId);
                break;
            case this.controlRememberMeKey:
                this._controlPairCollection.showGroup(this.controlPasswordGroupId);
                break;
            case this.buttonResetKey:
                if (this.isLogin) {
                    this._controlPairCollection.focus(this.controlRememberMeGroupId);
                } else if (this.isRegister) {
                    this._controlPairCollection.showGroup(this.controlEmailConfirmGroupId);
                }
                break;
            case this.buttonSendKey:
                this.buttonResetDOM.focus();
                break;
            }
    }
    ...
    private tabOrEnter = (event: KeyboardEvent, key: string) => {
        let preventDefault = true;
        switch(key) {
            case this.controlUsernameKey:
                if (event.shiftKey) {
                    preventDefault = false;
                } else {
                    this.nextControl(key);
                }
                break;
            case this.buttonResetKey:
            case this.buttonSendKey:
                if (event.shiftKey) {
                    this.previousControl(key);
                } else {
                    preventDefault = false;
                }
                break;
            default:
                if (event.shiftKey) {
                    this.previousControl(key);
                } else {
                    this.nextControl(key);
                }
                break;
        }
        return preventDefault;
    }
    ...
 }
(f) Introducing a next and previous method that we can use to navigate between controls regardless of whether the user clicks the buttons or uses the keyboard for navigation.

control-pair.ts (2)

...
export class ControlPair {
    ...
    private _activeId: string = null;
    ...
    constructor(config: IControlPairConfig) {
        ...
        this._activeId = this._left.id;
    }
    ...
    public showGroup = (id: string) => {
        ...
        if (this._activeId === id) {
            group.showGroup();
            this.focus(id);
            return;
        }

        this._activeId = id;
        ...
    }
}
(g) We need to keep track of which control is visible so we do not play the show animation if it is already visible.
Advertisement

Too bad the control flow isn't that simple

Unfortunately the control flow that we have created is only the behaviour we want if all of the controls are in a valid state and the user is using the keyboard to navigate. In the following images the green arrows represent the flow if the control is valid and the red if it is invalid.

Keyboard

Image (h) shows the flow for logging in and image (i) shows the flow for registering. One important consideration we have for keyboard navigation is we want there to always be a path to the reset button.

Control flow using the keyboard when the user is logging in.
(h) Control flow using the keyboard when the user is logging in.
Control flow using the keyboard when the user is registering.
(i) Control flow using the keyboard when the user is registering.

Button

In both (j) and (k) a red arrow that starts and ends on the same control simply means that we will refocus the control if the user presses the button and the control is invalid. Unlike when the user is using the keyboard to navigate we no longer need to have a path to the reset button if the user is clicking on the group buttons. Another important change is that if the user presses one of the buttons belonging to a confirm group then we will animate back to the first control whether or not the confirm control is valid. This behaviour is represented by the orange arrows.

Control flow when clicking on the buttons and logging in.
(j) Control flow when clicking on the buttons and logging in.
Control flow when clicking on the buttons and registering.
(k) Control flow when clicking on the buttons and registering.
  • WebUi
    • Source
      • components
        • authenticator
          • authenticator.component.ts

To achieve the behaviour we discussed above we will start by modifying the onClickGroupButton method. If the key belongs to one of the confirm controls then we just navigate to the previous control regardless of whether the current control is valid or not. Otherwise if it is invalid we just refocus it and if it is valid we move to the next control. We are also making a little helper method to pick which button should be focused based on whether the form is valid or not. Lastly we need to modify the nextControl and previousControl methods to account for the flows shown in the previous images.

authenticator.component.ts (3)

...
export class AuthenticatorComponent ... {
    ...
    public onClickGroupButton = (key: string) => {
        switch(key) {
            case this.controlPasswordConfirmKey:
            case this.controlEmailConfirmKey:
                this.previousControl(key);
                break;
            default:
                const control = this[key] as FormControl;
                if (control.invalid) {
                    this._controlPairCollection.focus(this._keyToGroupIdMap[key]);
                    return;
                }
                this.nextControl(key);
                break;
        }
    }
    ...
    private focusButton = () => {
        if (this.authForm.valid) {
            this.buttonSendDOM.focus();
        } else {
            this.buttonResetDOM.focus();
        }
    }
    ...
    private nextControl = (key: string) => {
        switch (key) {
            ...
            case this.controlPasswordKey:
                if (...) {
                    ...
                } else if (...) {
                    if (this.controlPassword.valid) {
                        this._controlPairCollection.showGroup(this.controlPasswordConfirmGroupId);
                    } else {
                        this._controlPairCollection.showGroup(this.controlEmailGroupId);
                    }
                }
                break;
            case this.controlPasswordConfirmKey:
                if (this.controlPasswordConfirm.valid) {
                    this._controlPairCollection.showGroup(this.controlEmailGroupId);
                } else {
                    this._controlPairCollection.showGroup(this.controlPasswordGroupId);
                }
                break;
            case this.controlEmailKey:
                if (this.controlEmail.valid) {
                    this._controlPairCollection.showGroup(this.controlEmailConfirmGroupId);
                } else {
                    this.focusButton();
                }
                break;
            case this.controlEmailConfirmKey:
                if (this.controlEmailConfirm.valid) {
                    this.focusButton();
                } else {
                    this._controlPairCollection.showGroup(this.controlEmailGroupId);
                }
                break;
            case this.controlRememberMeKey:
                this.focusButton();
                break;
        }
    }

    private previousControl = (key: string) => {
        switch (key) {
            ...
            case this.controlEmailKey:
                if (this.controlPassword.valid) {
                    this._controlPairCollection.showGroup(this.controlPasswordConfirmGroupId);
                } else {
                    this._controlPairCollection.showGroup(this.controlPasswordGroupId);
                }
                break;
            ...
            case this.buttonResetKey:
                if (...) {
                    ...
                } else if (...) {
                    if (this.controlEmail.valid) {
                        this._controlPairCollection.showGroup(this.controlEmailConfirmGroupId);
                    } else {
                        this._controlPairCollection.showGroup(this.controlEmailGroupId);
                    }
                }
                break;
            ...
        }
    }
    ...
}
(l) Modification of our typescript class in order to enforce the control flow shown in our control flow images.
Exciton Interactive LLC
Advertisement