Skip to content

Commit

Permalink
feat: Add animations to show verification grid usage (#265)
Browse files Browse the repository at this point in the history
Fixes: #107
Fixes: #108
  • Loading branch information
hudson-newey authored Jan 14, 2025
1 parent 1439706 commit c7ce400
Show file tree
Hide file tree
Showing 60 changed files with 2,667 additions and 503 deletions.
24 changes: 24 additions & 0 deletions dev/empty-verification-grid.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Components - Empty Verification Grid</title>
<script type="module" src="/src/components/index.ts"></script>
</head>

<body>
<oe-verification-grid></oe-verification-grid>

<p>
This dev page tests a verification grid with no slotted content. This means that there are no decision buttons or
tile templates.
</p>

<p>
We should see that the verification grid is still functional, and that the skip button is still present, but there
should be warning and errors displayed on the page indicating to the user that their verification grid is set up
incorrectly.
</p>
</body>
</html>
2 changes: 1 addition & 1 deletion dev/theme/theme.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import theming from "../..src/helpers/themes/theming.css?inline";
import theming from "../../src/helpers/themes/theming.css?inline";
import globalStyles from "../../src/helpers/themes/globalStyles.css?inline";

export function appendStyles(content: string): void {
Expand Down
11 changes: 11 additions & 0 deletions dev/verification-grid.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ <h1>Large text test</h1>
<button onclick="changeGridSize();">Change verification grid host size</button>
<button onclick="changeSelectionBehavior();">Change selection behavior</button>
<button onclick="changeCallback();">Use custom callback</button>
<button onclick="removeDecisions();">Remove Decision Buttons</button>

<div class="host-application-tester">
<p>
Expand Down Expand Up @@ -136,6 +137,16 @@ <h1>Large text test</h1>
verificationGridElement.setAttribute("key", "AudioLink");
}
}

function removeDecisions() {
const verificationElements = document.getElementsByTagName("oe-verification");
const classificationElements = document.getElementsByTagName("oe-classification");
const decisionElements = [...verificationElements, ...classificationElements];

for (const element of decisionElements) {
element.remove();
}
}
</script>
</body>
</html>
11 changes: 10 additions & 1 deletion docs/code-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ and [code-workspace](/webcomponents.code-workspace).
e.g. `@query("#selector") private element!: Readonly<Element>`
6. Prefer to use internal state
7. All methods on a web component that returns a HTML, or SVG template must be
prepended with "Template"(e.g. `private decisionPromptTemplate(): TemplateResult<1>`)
prepended with "Template" (e.g. `private decisionPromptTemplate(): TemplateResult`)

## CSS Parts

Expand All @@ -75,6 +75,15 @@ following format:
This ensure that the user can change the default styles of css part targeted
elements.

## CSS Animations

Because `@keyframe` declarations cannot be lexically scoped in CSS, you should
**always** prefix keyframe identifiers with their scoping.

Example: For an animation that targets a grid tile element, the scoping prefix
"`grid-tile`" could be used in a keyframe declaration such as
`@keyframe grid-tile-selection-animation`.

## JsDoc

Each custom element must be decorated with a JsDoc comment that contains a
Expand Down
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<section>
<a href="/dev/verification-grid.html">Verification Grid</a>
<a href="/dev/verification-grid-callback.html">Verification Grid Callback</a>
<a href="/dev/empty-verification-grid.html">Empty Verification Grid</a>
</section>

<section>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"main": "./dist/components.js",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "build:components && build:docs",
"build": "pnpm build:components && pnpm build:docs",
"dev:docs": "concurrently \"eleventy --config eleventy.config.js --watch --incremental\" \"pnpm start:docs\"",
"start:docs": "vite ./dist/docs/ --config ./vite.config.ts",
"build:docs": "rimraf build/ && cem analyze --litelement --globs \"src/components/**/*.ts\" && vite build && eleventy --config=eleventy.config.js",
Expand Down
286 changes: 286 additions & 0 deletions src/components/bootstrap-modal/bootstrap-modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
import { customElement, query, state } from "lit/decorators.js";
import { AbstractComponent } from "../../mixins/abstractComponent";
import { html, HTMLTemplateResult, LitElement, unsafeCSS } from "lit";
import { DecisionComponent } from "../decision/decision";
import { when } from "lit/directives/when.js";
import { loop } from "../../helpers/directives";
import { KeyboardShortcut } from "../../templates/keyboardShortcut";
import { BootstrapSlide } from "./slides/bootstrapSlide";
import { decisionsSlide } from "./slides/decisions/decisions";
import { selectionSlide } from "./slides/selection/selection";
import { pagingSlide } from "./slides/paging/paging";
import { shortcutsSlide } from "./slides/shortcuts/shortcuts";
import { consume } from "@lit/context";
import { VerificationGridInjector } from "verification-grid/verification-grid";
import { injectionContext } from "../../helpers/constants/contextTokens";
import { decisionColors } from "../../helpers/themes/decisionColors";
import { SlCarousel } from "@shoelace-style/shoelace";
import { DecisionOptions } from "../../models/decisions/decision";
import { advancedShortcutsSlide } from "./slides/advanced-shortcuts/advanced-shortcuts";
import { CssVariable } from "../../helpers/types/advancedTypes";
import bootstrapDialogStyles from "./css/style.css?inline";

// styles for individual slides
import decisionSlideStyles from "./slides/decisions/styles.css?inline";
import pagingSlideStyles from "./slides/paging/styles.css?inline";
import selectionSlideStyles from "./slides/selection/styles.css?inline";
import shortcutSlideStyles from "./slides/shortcuts/styles.css?inline";
import advancedShortcutStyles from "./slides/advanced-shortcuts/styles.css?inline";

/*
A local storage key that when set, will cause the bootstrap modal to not
automatically open on load.
This does not prevent the modal from being opened manually through the
verification grids information icon or the bootstraps open() method.
*/
const autoDismissBootstrapStorageKey = "oe-auto-dismiss-bootstrap";

/**
* @description
* A dialog that contains informative animations about the verification grid and
* how to use it.
*
* @event open - Dispatched when the dialog is opened
* @event close - Dispatched when the dialog is closed
*/
@customElement("oe-verification-bootstrap")
export class VerificationBootstrapComponent extends AbstractComponent(LitElement) {
public static styles = [
unsafeCSS(bootstrapDialogStyles),
decisionColors,

unsafeCSS(decisionSlideStyles),
unsafeCSS(pagingSlideStyles),
unsafeCSS(selectionSlideStyles),
unsafeCSS(shortcutSlideStyles),
unsafeCSS(advancedShortcutStyles),
];

@consume({ context: injectionContext, subscribe: true })
@state()
private injector!: VerificationGridInjector;

// because this is an internal web component, we can use the state decorator
// because it doesn't matter if the property name is minified
// we would usually use a property decorator for public properties because the
// property name is important to people using the web component
// however, in this case we are not exposing the properties to the client host
@state()
public decisionElements!: DecisionComponent[];

@state()
public hasVerificationTask!: boolean;

@state()
public hasClassificationTask!: boolean;

@state()
public isMobile!: boolean;

@state()
private slides: BootstrapSlide[] = [];

@query("#dialog-element")
private dialogElement!: HTMLDialogElement;

@query("#tutorial-slide-carousel")
private tutorialSlideCarouselElement!: SlCarousel;

private isAdvancedDialog = false;

public get open(): Readonly<boolean> {
return this.dialogElement.open;
}

private get autoDismissPreference(): boolean {
return localStorage.getItem(autoDismissBootstrapStorageKey) === null;
}

private set autoDismissPreference(value: string) {
localStorage.setItem(autoDismissBootstrapStorageKey, value);
}

private get decisionShortcuts(): ReadonlyArray<KeyboardShortcut> {
return this.decisionElements.flatMap((element) => element.shortcutKeys());
}

// the demo decision button can be undefined if the user creates a verification
// grid with no decision buttons
private get demoDecisionButton(): Readonly<DecisionComponent | undefined> {
// In the bootstrap slide animations, we show how to use a decision button
// by displaying the first decision button inside the bootstrap animations.
//
// We do not show all the decision elements inside the bootstrap animations
// in an attempt to reduce clutter, and make the animations easier to code.
//
// Only having one decision button is easier because we always know where it
// will be inside the svg animation, meaning that when we animate the mouse
// clicking on the decision button, we always know where it will be.
return this.decisionElements[0];
}

public firstUpdated(): void {
if (this.autoDismissPreference) {
this.showTutorialDialog();
}
}

public showTutorialDialog(): void {
this.isAdvancedDialog = false;

const slides: BootstrapSlide[] = [
decisionsSlide(this.hasVerificationTask, this.hasClassificationTask, this.demoDecisionButton),
selectionSlide(this.hasClassificationTask, this.demoDecisionButton),
pagingSlide(),
];

// if the user is on a mobile device, we don't need to bother showing
// the keyboard shortcuts slide
// by conditionally adding it to the slides array, we can reduce the amount
// of information that needs to be consumed by the user
//
// additionally, if there are no decision shortcut keys, we don't display
// the shortcut bootstrap slide
if (!this.isMobile && this.decisionShortcuts.length > 0) {
slides.push(shortcutsSlide(this.decisionShortcuts, this.hasClassificationTask));
}

this.showDialog(slides);

// Whenever the dialog is opened, we want to reset the slide carousel back
// to the start so that if the user re-opens the bootstrap modal through the
// verification grids "help" button or the "showTutorialDialog()" method,
// the tutorial will start from the beginning again.
// If we did not reset the tutorial carousel back to the start, the tutorial
// would start from the slide they close it on.
this.tutorialSlideCarouselElement?.goToSlide(0);
}

public showAdvancedDialog(): void {
this.isAdvancedDialog = true;

const slides = [advancedShortcutsSlide()];
this.showDialog(slides);
}

private closeDialog(): void {
this.autoDismissPreference = "true";
this.dialogElement.close();
this.dispatchEvent(new CustomEvent("close"));
}

// this method is private because you should be explicitly opening the modal
// through the showTutorialDialog() and showAdvancedDialog() methods
private showDialog(slides: BootstrapSlide[]): void {
this.slides = slides;
this.dialogElement.showModal();
this.dispatchEvent(new CustomEvent("open"));
}

private positiveDecisionColor(): Readonly<CssVariable> {
const decisionModel = this.demoDecisionButton?.decisionModels[DecisionOptions.TRUE];
if (!decisionModel) {
console.warn("Bootstrap could not determine positive decision color. Falling back to --verification-true");
return "--verification-true";
}

return this.injector.colorService(decisionModel);
}

private negativeDecisionColor(): Readonly<CssVariable> {
const decisionModel = this.demoDecisionButton?.decisionModels[DecisionOptions.FALSE];
if (!decisionModel) {
console.warn("Bootstrap could not determine negative decision color. Falling back to --verification-false");
return "--verification-false";
}

return this.injector.colorService(decisionModel);
}

private renderSlide(slide: BootstrapSlide): HTMLTemplateResult {
return html`
<div
class="slide-content"
style="
--positive-color: var(${this.positiveDecisionColor()});
--negative-color: var(${this.negativeDecisionColor()});
"
>
<div class="slide-header">
<h2 class="slide-title">${slide.title}</h2>
${when(slide.description, () => html`<p class="slide-description">${slide.description}</p>`)}
</div>
${slide.slideTemplate}
</div>
`;
}

private slideFooterTemplate(): HTMLTemplateResult {
if (this.isAdvancedDialog) {
return html`<button class="oe-btn-secondary" @click="${this.showTutorialDialog}">Replay tutorial</button>`;
}

return html`<button class="oe-btn-primary" @click="${this.closeDialog}">Get started</button>`;
}

private slidesTemplate(): HTMLTemplateResult {
if (!this.slides?.length) {
return html`<strong>No slides to display</strong>`;
}

// If there is only one slide in the carousel, we don't need to show the
// navigation arrows, the pagination bubbles, or enable mouse dragging
// support.
// By disabling these features, we get more room on smaller screens and
// remove UI elements that can cause confusion when there is only one
// bootstrap slide.
const showCarouselPagination = this.slides.length > 1;

return html`
<sl-carousel
id="tutorial-slide-carousel"
class="carousel"
?navigation="${showCarouselPagination}"
?pagination="${showCarouselPagination}"
?mouse-dragging="${showCarouselPagination}"
>
${loop(
this.slides,
(slide, { last }) => html`
<sl-carousel-item class="carousel-item">
${this.renderSlide(slide)}
<div class="slide-footer">${when(last, () => this.slideFooterTemplate())}</div>
</sl-carousel-item>
`,
)}
</sl-carousel>
`;
}

public render(): HTMLTemplateResult {
console.debug("re-rendering", this.hasVerificationTask, this.hasClassificationTask);
return html`
<dialog id="dialog-element" @pointerdown="${() => this.closeDialog()}">
<div class="dialog-container" @pointerdown="${(event: PointerEvent) => event.stopPropagation()}">
<header class="dialog-header">
<button
class="oe-btn-secondary close-button"
@click="${() => this.closeDialog()}"
data-testid="dismiss-bootstrap-dialog-btn"
>
x
</button>
</header>
<div class="dialog-content">${this.slidesTemplate()}</div>
</div>
</dialog>
`;
}
}

declare global {
interface HTMLElementTagNameMap {
"oe-verification-bootstrap": VerificationBootstrapComponent;
}
}
Loading

0 comments on commit c7ce400

Please sign in to comment.