Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 88 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,111 @@

## Overview

We'd like you to implement a modal dialog component using the codebase provided. We've kept the brief intentionally
loose in places - we're interested in the decisions you make, not just the end result.
Implement a modal dialog component using the codebase provided.

## The task

Using the existing codebase as your starting point, implement a modal dialog component. A rough reference for behaviour
and appearance can be found here: [stripe.com](https://stripe.com/au) (see expand icon on homepage cards).

Consider this a starting point, not a strict spec. Your implementation should fit the patterns and conventions already
established in the codebase.
Implementation should fit the patterns and conventions already established in the codebase.

## Requirements

The dialog should:
## What to submit

- Open and close correctly
- Avoid using any external UI libraries - we'd like to see your own implementation
- Be keyboard accessible - including Escape to close, and correct focus management when opening and closing
- Follow the existing component patterns, naming conventions, and file structure in the codebase
- Be written in TypeScript
- [x] See the [CONTRIBUTING.md](./CONTRIBUTING.md) file for setup instructions and guidelines.

## Process

- [x] Read through task 10:30 and plan
- [x] Onboard & check
- [x] pnpm audit - (PMG) notes critical npm packages: look at --fix
- [x] Plan 30m and Discovery
- [x] need to install oxc.oxc-vscode formatter to match code styles.

- [x] Run tests on current code.
- [x] `pnpm test` - worked - [x] `pnpm lint` - Found 40 errors in 4 files.

```
Errors Files
10 src/elements.ts:3
1 src/Utility/Elements/breakpoint-loader.ts:75
1 src/Utility/Elements/io-loader.ts:58
28 src/Utility/Elements/keyboard.ts:7
```

- [x] `pnpm test`

```
Snapshots 1 failed
Test Files 11 failed | 28 passed (39)
Tests 57 failed | 54 passed (111)
```

Deciding to move on...

- find components and read through current component/theme implementations
- [x] Stripe example Components used
- [x] Buttons with arrow icon and links
- [x] Expand / Close icons
- [x] Grids: 2/3, 1/3, full
- [x] Cards and backgrounds
- [x] Text, quotes, logo, headings

- [x] Ideation 30m - decide on way forward
- [x] Can I use dialog element?
- [x] But if the implementation should be written in TypeScript - then change to div and custom open/close.

- [x] Iterate
- [x] Test assumptions
- [x] Create storybook component
- [x] Style a Story.
- [x] Check Accessibility
- [x] Add modifiers for Dialog storybook component
- [] Make a Story with multiple dialogs to test open / close of each on a page.
- [] Style dialogs on a page and ::backdrop.
- [ ] update snapshots & Write interaction tests
- [x] docs
- [x] commits

## Requirement checklist

## What we're not prescribing
The dialog should:

We've deliberately left the following open - please make your own decisions and note them down:
- [x] Open and close correctly
- [x] Avoid using any external UI libraries - we'd like to see your own implementation
- [x] Be keyboard accessible - including Escape to close,
- [ ] And correct focus management when opening and closing
- [1/2] Follow the existing component patterns, naming conventions, and file structure in the codebase
- [x] Be written in TypeScript << GOT STUCK HERE on the open / closer functions >>

- Mobile behaviour and breakpoints
- What happens to page scroll when the dialog is open
- Animation and transition behaviour
- How the trigger element is handled
- [] Mobile behaviour and breakpoints
- [x] I'd want to fix main background, so scroll only affects the content of the dialog when open
- [] Animation and transition behaviour
- [x] How the trigger element is handled

## What to submit
- [x] Run tests, linters
- [x] Docs
- [x] Commit

Along with your code, please include a brief README (a few bullet points is fine) covering:
## Submission

- Any assumptions you made where the spec was unclear
- Any trade-offs or decisions you'd approach differently with more time
- Anything you noticed in the existing codebase you'd flag in a code review

## Time
- [x] whether to use dialog element
- https://caniuse.com/?search=dialog
- https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog > Global 96.86%

We'd suggest around 2–3 hours, but there's no hard limit. We're more interested in quality and thoughtfulness than
completeness - if you run out of time, notes on what you'd do next are just as valuable.
- [] or latest popover with polyfills for browsers not yet compatible.
- [x] But suggestion is to use TypeScript, so will do a more manual custom dialog

## Follow-up
- [x] Any trade-offs or decisions you'd approach differently with more time
- Get more familiar with the available utility styles and colours and spacing css vars.
- Read the Function docs!
- Investigate and Fix the other Failed tests

We'll schedule a short call to walk through your submission together. Be prepared to talk through your decisions - there
are no trick questions, we just want to understand your thinking.
- [x] Anything you noticed in the existing codebase you'd flag in a code review
- The format/linter changes single quotes back to double quotes (but "semi": false, inoxfmtrc.json)
- NPM packages security audit - 1 critical package

## Ready to start?
- [x] include a README (this):

Great! Please see the [CONTRIBUTING.md](./CONTRIBUTING.md) file for setup instructions and guidelines on how to submit
your work. We look forward to seeing your implementation!
https://github.com/stewest/frontend-challenge
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,5 @@
"engines": {
"node": ">= 22.0"
},
"packageManager": "pnpm@10.32.1"
"packageManager": "pnpm@10.33.0"
}
93 changes: 93 additions & 0 deletions src/Component/Dialog/Dialog.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Meta, StoryObj } from "@storybook/html-vite"
import Component from "./dialog.twig"
import Heading from "../../Atom/Heading/heading.twig"
import "./Elements/Dialog"
import "./dialog.css"
import "../Card/card.css"
import { Heading as HeadingType, HeadingTypes, WysiwygText } from "@pnx-mixtape/ids-shape"

export type Dialog = {
title?: HeadingType
content: WysiwygText
dialogTitle: HeadingType
dialogContent: WysiwygText
state?: boolean
id?: string
toggleText?: string
}

const meta: Meta<Dialog> = {
tags: ["autodocs", "ids-mvp"],
component: Component,
args: {
title: Heading({
title: "Closed state 'Dialog card' element title",
as: HeadingTypes.TWO,
}),
dialogTitle: Heading({
title: "This is the open Custom 'Dialog' Element title",
as: HeadingTypes.TWO,
}),
content:
"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>",
dialogContent:
"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>",
state: false,
},
argTypes: {
title: {
description:
"Optional [Heading](/?path=/docs/atom-heading--docs) component, displayed above the Dialog.",
control: "text",
},
content: {
description: "Content.",
control: "text",
type: {
name: "string",
},
},
dialogTitle: {
description:
"Optional [Heading](/?path=/docs/atom-heading--docs) component, displayed above the Dialog.",
control: "text",
},
dialogContent: {
description: "Content.",
control: "text",
type: {
name: "string",
},
},
state: {
description: "Dialog open or closed",
control: "boolean",
table: {
defaultValue: { summary: "closed" },
},
},
},
}

export default meta
type Story = StoryObj<Dialog>

export const Dialog: Story = {
args: {
content:
"<p>This is the default story content text inside the dialog card part 1. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>",
dialogContent:
"<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p><p>Aromatic aroma con panna, crema so coffee robust coffee barista, café au lait trifecta that strong blue mountain cortado aftertaste. Aroma extraction french press, skinny sweet, blue mountain cup roast barista, beans, extra cappuccino mug crema strong. Americano caffeine white, con panna saucer sit, con panna eu, carajillo aftertaste kopi-luwak, body aftertaste cup single origin café au lait saucer</p><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>",
},
}

/**
* Open by default dialog.
*/
export const StateOpen: Story = {
args: {
...meta.args,
state: true,
id: "open-dialog",
},
}
97 changes: 97 additions & 0 deletions src/Component/Dialog/Elements/Dialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Dialog
* @file Support opening on hash, adding an ID attribute and toggling on print.
*/

import { makeAnchor } from "../../../Utility/utilities"

export default class Dialog extends HTMLElement {
internals_: ElementInternals
controller: AbortController

constructor() {
super()
this.internals_ = this.attachInternals()
this.controller = new AbortController()
}

connectedCallback(): void {
if (!this.dialogElement || !this.openTrigger || !this.closerTigger) return

const { signal }: AbortController = this.controller

document.addEventListener("click", this.handleClick, {
signal,
})

document.addEventListener("keydown", event => {
if (event.code === "Escape") {
this.handleClose()
}
})
}

disconnectedCallback(): void {
this.controller.abort()
}

handleClick = ({ target }) => {
if (target === this.openTrigger) {
this.handleOpen()
}
if (target === this.closerTigger) {
this.handleClose()
}
}

handleOpen = (): void => {
this.dialogElement.setAttribute("data-state", "open")
}

handleClose = (): void => {
this.dialogElement.setAttribute("data-state", "closed")
}

get dialogElement(): HTMLElement | null {
const dialogElement: HTMLElement | null = this.querySelector(".mx-dialog__element")

if (!dialogElement) {
throw new Error(`${this.localName} must contain an element with .mx-dialog__element class.`)
}
dialogElement.id = dialogElement.id || this.generatedId()
return dialogElement
}

get openTrigger(): HTMLElement | null {
const trigger: HTMLElement | null = this.querySelector(".mx-dialog__toggle button")

if (!trigger) {
throw new Error(`${this.localName} must contain an element with .mx-dialog__toggle>.`)
}
return trigger
}

get closerTigger(): HTMLElement | null {
const trigger: HTMLElement | null = this.querySelector(".mx-dialog__element__close button")

if (!trigger) {
throw new Error(
`${this.localName} must contain an element with class mx-dialog__element__close>.`,
)
}
return trigger
}

generatedId = (): string => {
const string: string | undefined = this.openTrigger?.textContent?.trim()
return !string ? "" : makeAnchor(string)
}
}

if (!customElements.get("mx-dialog")) customElements.define("mx-dialog", Dialog)

declare global {
interface HTMLElementTagNameMap {
"mx-dialog": Dialog
}
}
Loading