|
| 1 | +# Drawer |
| 2 | + |
| 3 | +A drawer is a sliding panel that appears from the side of the screen, typically used for navigation, filters, or additional content. |
| 4 | +It is built on top of the native `<dialog>` element, similar to modals, but designed to slide in from a specific side of the viewport. |
| 5 | + |
| 6 | +<Callout> |
| 7 | + |
| 8 | +This pattern of a sliding drawer / sheet can be done with only the native `<dialog>` and some `<button>` to open it. This component is a _very_ small wrapper for this pattern -- primarily a state container for easily controlling the open state of the `<dialog>` element without wiring it up yourself. |
| 9 | + |
| 10 | +</Callout> |
| 11 | + |
| 12 | + |
| 13 | +<div class="featured-demo"> |
| 14 | + |
| 15 | +```gjs live preview no-shadow |
| 16 | +import { Drawer } from 'ember-primitives/components/drawer'; |
| 17 | +import { on } from '@ember/modifier'; |
| 18 | +
|
| 19 | +<template> |
| 20 | + <Drawer as |d|> |
| 21 | + <button {{on 'click' d.open}}>Open Drawer</button> |
| 22 | +
|
| 23 | + <d.Drawer class="not-prose"> |
| 24 | + <header> |
| 25 | + <h2>Example Drawer</h2> |
| 26 | + <button {{on 'click' d.close}}>Close</button> |
| 27 | + </header> |
| 28 | + <form method="dialog"> |
| 29 | + <main> |
| 30 | + Drawer content here |
| 31 | + <br> |
| 32 | + Because this is a native dialog element, it captures focus, |
| 33 | + and pressing escape will close the dialog and return focus to the |
| 34 | + original button. |
| 35 | + </main> |
| 36 | +
|
| 37 | + <footer> |
| 38 | + <button type="submit" value="confirm">Confirm</button> |
| 39 | + <button type="submit" value="create">Create</button> |
| 40 | + </footer> |
| 41 | + </form> |
| 42 | + </d.Drawer> |
| 43 | + </Drawer> |
| 44 | +
|
| 45 | + <link rel="stylesheet" href="https://unpkg.com/open-props/easings.min.css"/> |
| 46 | + <style> |
| 47 | + @scope { |
| 48 | + dialog { |
| 49 | + transition: |
| 50 | + display 0.125s allow-discrete, |
| 51 | + overlay 0.125s allow-discrete; |
| 52 | + animation: close 0.125s forwards; |
| 53 | +
|
| 54 | + &[open] { |
| 55 | + animation: open 0.25s forwards; |
| 56 | + } |
| 57 | + } |
| 58 | +
|
| 59 | + @keyframes open { |
| 60 | + from { |
| 61 | + opacity: 0; |
| 62 | + transform: translate(0, 100%); |
| 63 | + } |
| 64 | + to { |
| 65 | + opacity: 1; |
| 66 | + transform: translate(0, 0); |
| 67 | + } |
| 68 | + } |
| 69 | +
|
| 70 | + @keyframes close { |
| 71 | + from { |
| 72 | + opacity: 1; |
| 73 | + transform: translate(0, 0); |
| 74 | + } |
| 75 | + to { |
| 76 | + opacity: 0; |
| 77 | + transform: translate(0, 100%); |
| 78 | + } |
| 79 | + } |
| 80 | +
|
| 81 | + } |
| 82 | +
|
| 83 | + @scope { |
| 84 | + [data-repl-output] { |
| 85 | + display: block; |
| 86 | + } |
| 87 | + button { |
| 88 | + padding: 0.5rem 1rem; |
| 89 | + border-radius: 0.25rem;; |
| 90 | +
|
| 91 | + background-color: #2563eb; /* blue */ |
| 92 | + color: #ffffff; |
| 93 | +
|
| 94 | + transition: |
| 95 | + background-color 120ms ease-out, |
| 96 | + box-shadow 120ms ease-out, |
| 97 | + transform 120ms ease-out; |
| 98 | + } |
| 99 | +
|
| 100 | + button:hover { |
| 101 | + background-color: #1d4ed8; |
| 102 | + } |
| 103 | +
|
| 104 | + button:active { |
| 105 | + transform: translateY(1px); |
| 106 | + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.3) inset; |
| 107 | + } |
| 108 | + } |
| 109 | +
|
| 110 | + @scope { |
| 111 | + /* Basic reset for the dialog */ |
| 112 | + dialog { |
| 113 | + position: fixed; |
| 114 | + top: unset; |
| 115 | + bottom: 0px; |
| 116 | + color: black; |
| 117 | + background: white; |
| 118 | + margin: 0 auto; |
| 119 | + min-width: 80dvw; |
| 120 | + border-top-right-radius: 0.5rem; |
| 121 | + border-top-left-radius: 0.5rem; |
| 122 | +
|
| 123 | + main { |
| 124 | + padding: 1rem; |
| 125 | + } |
| 126 | +
|
| 127 | + header { |
| 128 | + display: flex; |
| 129 | + align-items: center; |
| 130 | + justify-content: space-between; |
| 131 | + padding: 0.5rem 1rem; |
| 132 | + border-bottom: 1px solid color-mix(in srgb, currentColor 10%, transparent); |
| 133 | +
|
| 134 | + h2 { |
| 135 | + font-size: 1.5rem; |
| 136 | + } |
| 137 | + } |
| 138 | +
|
| 139 | + footer { |
| 140 | + display: flex; |
| 141 | + justify-content: flex-end; |
| 142 | + gap: 2rem; |
| 143 | + padding: 1rem; |
| 144 | + } |
| 145 | + } |
| 146 | +
|
| 147 | + /* backdrop */ |
| 148 | + dialog::backdrop { |
| 149 | + background: color-mix(in srgb, #020617 60%, transparent); |
| 150 | + } |
| 151 | + } |
| 152 | + </style> |
| 153 | +</template> |
| 154 | +``` |
| 155 | + |
| 156 | +</div> |
| 157 | + |
| 158 | +## Install |
| 159 | + |
| 160 | +```hbs live |
| 161 | +<SetupInstructions @src="components/drawer.gts" /> |
| 162 | +``` |
| 163 | + |
| 164 | +## Anatomy |
| 165 | + |
| 166 | +```gjs |
| 167 | +import { Drawer } from 'ember-primitives/components/drawer'; |
| 168 | +
|
| 169 | +<template> |
| 170 | + <Drawer as |d|> |
| 171 | +
|
| 172 | + d.isOpen - boolean |
| 173 | + d.open - function |
| 174 | + d.close - function |
| 175 | +
|
| 176 | + <d.Drawer ...attributes> |
| 177 | + this is just the HTMLDialogElement |
| 178 | + </d.Drawer> |
| 179 | + </Drawer> |
| 180 | +</template> |
| 181 | +``` |
| 182 | + |
| 183 | +## API Reference |
| 184 | + |
| 185 | +```gjs live no-shadow |
| 186 | +import { ComponentSignature } from 'kolay'; |
| 187 | +
|
| 188 | +<template> |
| 189 | + <ComponentSignature |
| 190 | + @package="ember-primitives" |
| 191 | + @module="declarations/components/drawer" |
| 192 | + @name="Signature" /> |
| 193 | +</template> |
| 194 | +``` |
| 195 | + |
| 196 | +### State Attributes |
| 197 | + |
| 198 | +There is no root element for this component, so there are no state attributes to use. |
| 199 | +Since this component uses the `<dialog>` element, it will still use the `open` attribute. |
| 200 | + |
| 201 | +## Accessibility |
| 202 | + |
| 203 | +Once the drawer is open, the browser will focus on the first button (in this case, it's our close button on the drawer header) or any button with the autofocus attribute within the drawer. When you close the drawer, the browser restores the focus on the button we used to open it. |
| 204 | + |
| 205 | +### Keyboard Interactions |
| 206 | + |
| 207 | +Because this builds on `<dialog>`, all behaviors are already built in to the browser. |
| 208 | + |
| 209 | +Pressing the <kbd>esc</kbd> key closes the dialog, focusing the last-focused element before the dialog was opened. |
| 210 | + |
| 211 | +## References |
| 212 | + |
| 213 | +- [MDN HTMLDialogElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog) |
| 214 | +- [web.dev : Building a dialog component](https://web.dev/building-a-dialog-component/) |
| 215 | + |
| 216 | + |
0 commit comments