diff --git a/README.md b/README.md index 51ae8a3..fb893e0 100644 --- a/README.md +++ b/README.md @@ -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! diff --git a/src/Component/Dialog/Dialog.stories.ts b/src/Component/Dialog/Dialog.stories.ts new file mode 100644 index 0000000..588785c --- /dev/null +++ b/src/Component/Dialog/Dialog.stories.ts @@ -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 = { + 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: "

Tiles are just block cards without an image.

", + 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 + +export const Dialog: Story = {} diff --git a/src/Component/Dialog/__snapshots__/Dialog.stories.ts.snap b/src/Component/Dialog/__snapshots__/Dialog.stories.ts.snap new file mode 100644 index 0000000..793c8bc --- /dev/null +++ b/src/Component/Dialog/__snapshots__/Dialog.stories.ts.snap @@ -0,0 +1,69 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Dialog 1`] = ` +" + + +
+ +
+
+
+ + +

Card Title

+ +

Tiles are just block cards without an image.

+ + +
+
+ + Blurry bubbles + + +
+
+
+ +
+
+" +`; + +exports[`Mx Dialog Variants 1`] = ` +" + + +
+ +
+
+
+ + +

Card Title

+ +

Tiles are just block cards without an image.

+ + +
+
+ + Blurry bubbles + + +
+
+
+ +
+
+" +`; diff --git a/src/Component/Dialog/dialog.css b/src/Component/Dialog/dialog.css new file mode 100644 index 0000000..0774d5d --- /dev/null +++ b/src/Component/Dialog/dialog.css @@ -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; + } +} diff --git a/src/Component/Dialog/dialog.twig b/src/Component/Dialog/dialog.twig new file mode 100644 index 0000000..0dffc4c --- /dev/null +++ b/src/Component/Dialog/dialog.twig @@ -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) %} + + + +
+ {% if repeatContent > 0 %} + {% for _ in 1..repeatContent %} + {{ content }} + {% endfor %} + {% endif %} +
+