Skip to content

Instantly share code, notes, and snippets.

@michaelnero
Created May 20, 2021 10:29
Show Gist options
  • Save michaelnero/d9df3573bd61d86ea2bd98fdb11d09c0 to your computer and use it in GitHub Desktop.
Save michaelnero/d9df3573bd61d86ea2bd98fdb11d09c0 to your computer and use it in GitHub Desktop.

Revisions

  1. michaelnero created this gist May 20, 2021.
    19 changes: 19 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,19 @@
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="utf-8">
    <title>Dumber Gist</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
    <base href="/">
    </head>
    <!--
    Dumber Gist uses dumber bundler, the default bundle file
    is /dist/entry-bundle.js.
    The starting module is pointed to "main" (data-main attribute on script)
    which is your src/main.ts.
    -->
    <body>
    <my-app></my-app>
    <script src="/dist/entry-bundle.js" data-main="main"></script>
    </body>
    </html>
    5 changes: 5 additions & 0 deletions package.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,5 @@
    {
    "dependencies": {
    "aurelia": "latest"
    }
    }
    12 changes: 12 additions & 0 deletions src\components\listbox-button.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,12 @@
    <button
    id.bind="id"
    class.bind="classNames"
    ref="element"
    click.delegate="handleClick($event)"
    aria-haspopup="true"
    aria-controls.bind="ariaControls"
    aria-expanded.bind="ariaExpanded"
    aria-labelledby.bind="ariaLabelledBy"
    disabled.bind="disabled">
    <au-slot></au-slot>
    </button>
    57 changes: 57 additions & 0 deletions src\components\listbox-button.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,57 @@
    import { IPlatform, bindable } from "aurelia";
    import { IObservation, IEffect } from "@aurelia/runtime";
    import { IListboxContext, ListboxStates } from "./listbox-context";
    import { useId } from "./use-id";

    export class ListboxButton {
    @bindable() classNames: string;

    id: string;
    disabled: boolean;
    ariaControls: string;
    ariaExpanded: boolean;
    ariaLabelledBy: string;

    effect: IEffect;

    constructor(
    @IListboxContext private readonly context: IListboxContext,
    @IObservation private readonly observation: IObservation,
    @IPlatform private readonly platform: IPlatform) {

    this.id = `jg-listbox-button-${useId()}`;
    }

    bound() {
    this.context.buttonElement = this.element;
    this.effect = this.observation.run(() => {
    this.disabled = this.context.disabled;
    this.ariaControls = this.context.optionsElement?.id;
    this.ariaExpanded = this.context.state === ListboxStates.Open;
    this.ariaLabelledBy = this.context.labelElement?.id;
    });
    }

    unbinding() {
    this.context.buttonElement = null;

    this.effect?.stop();
    this.effect = null;
    }

    handleClick(event: MouseEvent) {
    if (this.context.disabled) return;
    if (this.context.state === ListboxStates.Open) {
    this.context.close();
    this.platform.taskQueue.queueTask(() => {
    this.context.buttonElement?.focus({ preventScroll: true });
    });
    } else {
    event.preventDefault();
    this.context.open();
    this.platform.domWriteQueue.queueTask(() => {
    this.context.optionsElement?.focus({ preventScroll: true });
    });
    }
    }
    }
    24 changes: 24 additions & 0 deletions src\components\listbox-context.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,24 @@
    import { DI } from "aurelia";
    import { observable } from "@aurelia/runtime";

    export enum ListboxStates { Open, Closed };

    export const IListboxContext = DI.createInterface<IListboxContext>(x => x.transient(ListboxContext));
    export interface IListboxContext extends ListboxContext { }
    class ListboxContext {
    @observable() disabled: boolean;
    @observable() state: ListboxStates;
    @observable() labelElement: HTMLLabelElement;
    @observable() buttonElement: HTMLButtonElement;
    @observable() optionsElement: HTMLUListElement;

    open(): void {
    // In reality, the implementation is more complicated than this.
    this.state = ListboxStates.Open;
    }

    close(): void {
    // In reality, the implementation is more complicated than this.
    this.state = ListboxStates.Closed;
    }
    }
    3 changes: 3 additions & 0 deletions src\components\listbox-label.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,3 @@
    <label ref="element" id.bind="id" class.bind="classNames" click.delegate="handleClick()">
    <au-slot></au-slot>
    </label>
    22 changes: 22 additions & 0 deletions src\components\listbox-label.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,22 @@
    import { bindable } from "aurelia";
    import { IListboxContext } from "./listbox-context";
    import { useId } from "./use-id";

    export class ListboxLabel {
    @bindable() classNames: string;
    @bindable() element: HTMLLabelElement;

    id: string;

    constructor(@IListboxContext private readonly context: IListboxContext) {
    this.id = `jg-listbox-label-${useId()}`;
    }

    bound() {
    this.context.labelElement = this.element;
    }

    handleClick() {
    this.context.buttonElement?.focus({ preventScroll: true });
    }
    }
    11 changes: 11 additions & 0 deletions src\components\listbox-options.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,11 @@
    <ul
    show.bind="isOpen"
    id.bind="id"
    class.bind="classNames"
    role="listbox"
    tabindex="0"
    ref="element"
    aria-activedescendant.bind="ariaActiveDescendant"
    aria-labelledby.bind="ariaLabelledBy">
    <au-slot></au-slot>
    </ul>
    30 changes: 30 additions & 0 deletions src\components\listbox-options.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,30 @@
    import { bindable } from "aurelia";
    import { IObservation, IEffect } from "@aurelia/runtime";
    import { IListboxContext } from "./listbox-context";

    export class ListboxOptions {
    @bindable() classNames: string;
    @bindable() element: HTMLUListElement;

    id: string;
    ariaLabelledBy: string;
    ariaActiveDescendant: string;

    constructor(@IListboxContext private readonly context: IListboxContext) {
    }

    bound() {
    this.context.buttonElement = this.element;
    this.effect = this.observation.run(() => {
    this.ariaLabelledBy = this.context.labelElement?.id ?? this.context.buttonElement?.id;
    // Also set this.ariaActiveDescendant
    });
    }

    unbinding() {
    this.context.buttonElement = null;

    this.effect?.stop();
    this.effect = null;
    }
    }
    3 changes: 3 additions & 0 deletions src\components\listbox.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,3 @@
    <div id.bind="id">
    <au-slot></au-slot>
    </div>
    12 changes: 12 additions & 0 deletions src\components\listbox.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,12 @@
    import { bindable, IPlatform } from "aurelia";
    import { newInstanceForScope } from "@aurelia/kernel";
    import { IListboxContext, ListboxStates } from "./listbox-context";
    import { useId } from "./use-id";

    export class Listbox {
    id: string;

    constructor(@newInstanceForScope(IListboxContext) context: IListboxContext) {
    this.id = `jg-listbox-${useId()}`;
    }
    }
    8 changes: 8 additions & 0 deletions src\components\use-id.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,8 @@
    let id = 0;
    function generateId(): number {
    return ++id;
    }

    export function useId(): number {
    return generateId();
    }
    4 changes: 4 additions & 0 deletions src\main.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,4 @@
    import Aurelia from 'aurelia';
    import { MyApp } from './my-app';

    Aurelia.app(MyApp).start();
    25 changes: 25 additions & 0 deletions src\my-app.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,25 @@
    <import from="./components/listbox"></import>
    <import from="./components/listbox-button"></import>
    <import from="./components/listbox-label"></import>
    <import from="./components/listbox-options"></import>

    <h1>${message}</h1>

    <listbox>
    <template au-slot>
    <div>
    <listbox-label class-names="sr-only">
    <template au-slot>Button label</template>
    </listbox-label>
    </div>

    <div>
    <listbox-button>
    <template au-slot>Click me</template>
    </listbox-button>
    </div>

    <listbox-options>
    </listbox-options>
    </template>
    </listbox>
    3 changes: 3 additions & 0 deletions src\my-app.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,3 @@
    export class MyApp {
    public message: string = 'Hello Aurelia 2!';
    }