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
55 changes: 11 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,22 @@

## 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.
Decided (for speed!) to lean on the web platforms modern [invoker command API](https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API) to achieve a feature complete dialog device. I've used this approach on a number of projects in production recently, utilising a few polyfills (not included in this submission).

## The task
> Any assumptions you made where the spec was unclear

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).
What content we should expect to render inside the dialog - for this submission I assumed other components, a Card in this case.

Consider this a starting point, not a strict spec. Your implementation should fit the patterns and conventions already
established in the codebase.
> Any trade-offs or decisions you'd approach differently with more time

## Requirements
- Much like the Stripe implementation, I'd endeavour to focus trap users inside the dialog where they can use the escape and 🅧 controls to close.
- I chose to use a Button component as the dialog trigger. Perhaps with more time I could explore augmenting other components with dialog triggering wrappers (much like the stripe.com example).
- I wanted to experiment with anchor positioning of the 🅧 button in the dialog, ideally I'd like it to remain fixed / non-scrollable with the content, but note the stripe.com example's 🅧 device _does_ scroll away, so stopped short.
- Perhaps the dialog variant is a little restrictive, maybe a set of modifiers would've been a better approach (peel from top, left, right)
- I have feelings around thumbable 🅧 buttons in dialogs on handheld devices - perhaps I'd move the close component to the bottom right on smaller viewports / containers.

The dialog should:
> Anything you noticed in the existing codebase you'd flag in a code review

- 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
Noticed one of the accordion tests were failing - an unexpected disabled attribute, perhaps rendered with javascript?

## What we're not prescribing

We've deliberately left the following open - please make your own decisions and note them down:

- Mobile behaviour and breakpoints
- What happens to page scroll when the dialog is open
- Animation and transition behaviour
- How the trigger element is handled

## What to submit

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

- 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

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.

## Follow-up

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.

## Ready to start?

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!
108 changes: 108 additions & 0 deletions src/Component/Dialog/Dialog.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Meta, StoryObj } from "@storybook/html-vite"
import Component from "./dialog.twig"
import "./dialog.css"
import { HeadingTypes } from "@pnx-mixtape/ids-shape"
import Card from "../Card/card.twig"
import "../Card/card.css"

// Deps.
import Heading from "../../Atom/Heading/heading.twig"
import Image from "../../Atom/Image/image.twig"
import { BackgroundStyles } from "../../enums"

// css
import "../Dialog/dialog.css"

export enum MxDialogVariants {
CENTERED = "centered",
PEELED_UP = "peeled-up",
}

export type MxDialogType = {
variant?: MxDialogVariants[]
id: string
content: string
repeatContent: number
}

const meta: Meta<MxDialogType> = {
tags: ["autodocs", "ids-mvp"],
component: Component,
args: {
id: "example-dialog",
repeatContent: 1,
content: Card({
title: Heading({
title: "Card Title",
as: HeadingTypes.THREE,
}),
image: Image({
src: "https://picsum.photos/id/56/558/418?grayscale",
alt: "Blurry bubbles",
width: 558,
height: 418,
}),
content: "<p>Tiles are just block cards without an image.</p>",
background: BackgroundStyles.BOX,
variant: [MxDialogVariants.PEELED_UP],
}),
},
argTypes: {
variant: {
description:
"The **peeled up** variant reveals the dialog from the bottom edge of the viewport, a little like the stripe.com device. The **centered** variant reveals the dialog from the center of the viewport.",
options: Object.values(MxDialogVariants),
control: "radio",
table: {
type: { summary: "enum" },
defaultValue: { summary: MxDialogVariants.PEELED_UP },
},
},
id: {
description:
"Must match between the open trigger and the dialog; used for `command` / `commandfor`.",
control: "text",
type: {
name: "string",
required: true,
},
table: {
type: { summary: "string" },
defaultValue: { summary: "example-dialog" },
},
},
content: {
description:
"Body markup inside the dialog. Can be the rendered output of Card, Callout, or any other Twig partial.",
control: "text",
type: {
name: "string",
required: true,
},
table: {
type: { summary: "WysiwygText | Card | Callout" },
subcategory: "Dialog content",
},
},
repeatContent: {
name: "Repeat content",
description:
"Renders `content` this many times inside `.mx-dialog__content`. Useful to test scrolling.",
control: { type: "number", min: 0, step: 1 },
type: {
name: "number",
required: true,
},
table: {
type: { summary: "number" },
defaultValue: { summary: "1" },
subcategory: "Dialog content",
},
},
},
}

export default meta
type Story = StoryObj<MxDialogType>

export const Dialog: Story = {}
69 changes: 69 additions & 0 deletions src/Component/Dialog/__snapshots__/Dialog.stories.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`Dialog 1`] = `
" <button class="mx-button" command="show-modal" commandfor="example-dialog">Open dialog</button>
<dialog class="mx-dialog" id="example-dialog">
<button command="close" commandfor="example-dialog" class="mx-button mx-button--icon-only" type="button">
<span class="mx-icon mx-icon--close"></span>
<span class="sr-only">Close dialog</span>
</button>
<div class="mx-dialog__content mx-vertical-flow">

<div class="mx-container">
<article class="mx-card mx-background--box">
<div class="mx-card__content mx-vertical-flow-flex">


<h3>Card Title</h3>

<div class="mx-text--lede mx-vertical-flow-flex"><p>Tiles are just block cards without an image.</p></div>


</div>
<figure class="mx-card__media">
<picture>
<img src="https://picsum.photos/id/56/558/418?grayscale" alt="Blurry bubbles" height="418" width="558">
</picture>

</figure>
</article>
</div>

</div>
</dialog>
"
`;

exports[`Mx Dialog Variants 1`] = `
" <button class="mx-button" command="show-modal" commandfor="example-dialog">Open dialog</button>
<dialog class="mx-dialog" id="example-dialog">
<button command="close" commandfor="example-dialog" class="mx-button mx-button--icon-only" type="button">
<span class="mx-icon mx-icon--close"></span>
<span class="sr-only">Close dialog</span>
</button>
<div class="mx-dialog__content mx-vertical-flow">

<div class="mx-container">
<article class="mx-card mx-background--box">
<div class="mx-card__content mx-vertical-flow-flex">


<h3>Card Title</h3>

<div class="mx-text--lede mx-vertical-flow-flex"><p>Tiles are just block cards without an image.</p></div>


</div>
<figure class="mx-card__media">
<picture>
<img src="https://picsum.photos/id/56/558/418?grayscale" alt="Blurry bubbles" height="418" width="558">
</picture>

</figure>
</article>
</div>

</div>
</dialog>
"
`;
62 changes: 62 additions & 0 deletions src/Component/Dialog/dialog.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Dialog
*/

@layer design-system.components {
.mx-dialog {
margin-inline: auto;
margin-block: auto;
transition-behavior: initial;
inline-size: min(100%, var(--container-max-width));
background-color: transparent;
border: none;
padding: 0;
opacity: 0;
transition:
display 0.25s allow-discrete,
overlay 0.25s allow-discrete,
opacity 0.25s;

&[open] {
opacity: 1;

@starting-style {
opacity: 0;
}
}

&::backdrop {
backdrop-filter: blur(10px);
}
}

.mx-dialog:is(.mx-dialog--peeled-up) {
margin-block-end: 0;
transform: translateY(30dvh);
transition:
display 0.25s allow-discrete,
overlay 0.25s allow-discrete,
opacity 0.25s,
transform 0.5s;

&[open] {
transform: translateY(0);

@starting-style {
transform: translateY(40dvh);
}
}
}

.mx-dialog__content {
anchor-name: --dialog-content;
padding-block: var(--spacing-m);
}

.mx-dialog > .mx-button {
position-anchor: --dialog-content;
position-area: top span-left;
position: absolute;
z-index: 1;
}
}
26 changes: 26 additions & 0 deletions src/Component/Dialog/dialog.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{% set baseClass = 'mx-dialog' %}
{% set classes = [
baseClass,
variant ? baseClass~"--"~variant : null,
] %}
{% set base_attributes = {
'closedby': 'any',
'id': id,
} %}
{% set dialog_id = base_attributes.id %}
{% set attributes = (attributes ?? create_attribute()).addClass(classes) %}
{% set attributes = attributes.setAttribute('id', dialog_id) %}
<button class="mx-button" command="show-modal" commandfor="{{ dialog_id }}">Open dialog</button>
<dialog{{ attributes }}>
<button command="close" commandfor="{{ dialog_id }}" class="mx-button mx-button--icon-only" type="button">
<span class="mx-icon mx-icon--close"></span>
<span class="sr-only">Close dialog</span>
</button>
<div class="mx-dialog__content mx-vertical-flow">
{% if repeatContent > 0 %}
{% for _ in 1..repeatContent %}
{{ content }}
{% endfor %}
{% endif %}
</div>
</dialog>