# Slideover

> A full-height panel that slides in from the side, e.g. to edit a record or fill in a multi-step form without leaving the page.

- Import: `import { Slideover } from '@thekestongroup/ui'`
- Group: Overlay
- Live example: https://ui.keston.co/storybook/?path=/story/components-overlay-slideover--default
- Source: https://github.com/thekestongroup/ui/blob/main/src/components/Slideover/Slideover.vue

## When to use

- Editing or adding something while keeping the page behind in context: edit a student, add an application.
- Multi-step forms (with `steps`).
- Longer side content: a notification list, a log.

## When not to use

- A small amount of extra content tied to one element: use Popover.
- A whole task that deserves its own address: use a page.

## Anatomy

| Part | Required | Notes |
|---|---|---|
| trigger | No | Default slot, e.g. a Button. Or control it with `v-model:open`. |
| overlay | No | Black 40% over the page. |
| panel | Yes | White, full height, up to 448px wide (full width on small screens). |
| header | Yes | Title (18px), description, × close button; 24px / 16px padding, line under it. |
| progress | With `steps` | A segment per step above the title, and "Step 2 of 4: Contact details" under it. |
| body | Yes | Scrolls on its own. |
| footer | No | Actions, e.g. Cancel / Save. With `steps`, Cancel or Previous and Next or Finish. |

## Properties

| Property | Type | Default | Description |
|---|---|---|---|
| title | string | — | The panel's heading and accessible name. Always set it. |
| description | string | — | Text under the title. |
| side | right \| left \| top \| bottom | right | Edge it slides in from. |
| open | boolean | — | Use `v-model:open` to control it. |
| overlay | boolean | true | Dim the page behind. |
| close | boolean | true | Show the × button. |
| dismissible | boolean | true | Close on Escape and clicking outside. If false, emits `close:prevent`. |
| modal | boolean | true | Block the page behind while open. |
| initialFocus | none \| auto \| close | none | Where focus goes when it opens. See Accessibility. |
| steps | string[] | — | Step names, for a multi-step form. |
| step | number | 0 | Current step, from 0. Use `v-model:step`. Back to 0 each time it opens. |
| validateStep | (step) => boolean \| Promise | — | Checked before Next / Finish; return false to stay. Can be async, e.g. to save. |
| finishLabel | string | Finish | Text of the last step's button. |

| Event | When |
|---|---|
| finish | Finish was pressed on the last step and `validateStep` passed. |
| close:prevent | Closing was attempted while not dismissible. |

## Behaviour

- Slides in over 300ms and out over 200ms; with reduced motion it fades instead.
- The page behind doesn't scroll while it's open.
- **Multi-step forms:** Next checks the step with `validateStep`, then moves on; Previous goes back; the first step shows Cancel. Enter in a text field presses Next / Finish (after InputDate's own first Enter). Each new step starts scrolled to the top.

## Accessibility

- A dialog named by the title and described by the description. Focus is kept inside while it's open, and returns to the trigger on close.
- `initialFocus`: `none` focuses the panel itself, so screen readers read from the top; `auto` focuses the first field (not on touch screens, so the keyboard doesn't pop up); `close` focuses ×.
- **Multi-step forms:** the step is part of the description when it opens. After Enter, focus moves to the next step's first control, which sits in a group named "Step 3 of 4: Contact". After Next or Previous, the step is announced. It's read once each time.

## Platform adaptation

| Platform | Implementation | Differences |
|---|---|---|
| Web / desktop (Vue) | `Slideover.vue` | Reka UI Dialog. |
| iOS (SwiftUI) | — | `.sheet` on iPhone; `.inspector` on iPad and Mac. |
| Android (Compose) | — | `ModalBottomSheet` on phones; a side sheet on large screens. |

## Tokens used

| Token | Used for |
|---|---|
| `color-white` / `color-black` | Panel, overlay, lines |
| `color-primary` | Step segments |
| `animate-slide-in-*` / `animate-slide-out-*` | Sliding in and out |

## API (from the source)

The component's exact props, model, events, slots and types, with their documentation comments.

```ts
// Slideover.vue
/**
 * A full-height panel that slides in from the side of the screen, e.g. to edit
 * a record without leaving the page. Built on Reka UI's Dialog: focus is kept
 * inside while open, Escape and clicking the overlay close it, the page behind
 * doesn't scroll, and it is announced as a dialog.
 *
 * Slots: default (the trigger, e.g. a Button), header (replaces title,
 * description and close), title, description, body (the scrolling middle),
 * footer (e.g. Cancel / Save), content (replaces everything inside the panel).
 * Slots that close it get `{ close }`.
 *
 * Multi-step forms: pass `steps` (one name per step) and show the current
 * step's fields in the body slot, which gets `{ step }`. The header shows a
 * segment per step and "Step 2 of 4: Contact details"; the footer gets
 * Cancel / Previous and Next / Finish, unless you fill the footer slot.
 * Enter in a text field presses Next / Finish.
 */
export interface SlideoverProps {
  /** Heading of the panel. Also its accessible name, so always set it (or the title slot). */
  title?: string
  /** Text under the title. */
  description?: string
  /** Edge it slides in from. */
  side?: 'right' | 'left' | 'top' | 'bottom'
  /** Dim the page behind it. */
  overlay?: boolean
  /** Show the × close button in the header. */
  close?: boolean
  /** Close on Escape and on clicking outside. If false, emits close:prevent instead. */
  dismissible?: boolean
  /** Block interaction with the page behind while open. */
  modal?: boolean
  /** Open when first shown. */
  defaultOpen?: boolean
  /**
   * Where focus goes when it opens:
   * - none (default): the panel itself, so screen readers announce the title
   *   and description and read from the top. Tab then moves to the first
   *   control.
   * - auto: an element marked `autofocus`, else the first form field, else
   *   the × button. On touch screens it skips text fields, so the keyboard
   *   doesn't pop up before the panel has been seen.
   * - close: the × button (or the first button, if there's no ×).
   */
  initialFocus?: 'auto' | 'close' | 'none'
  /** Render at the end of the page (true), inside a given element, or in place (false). */
  portal?: boolean | string | HTMLElement
  /** Passed to Reka's DialogContent. */
  content?: Omit<DialogContentProps, 'as' | 'asChild' | 'forceMount'>
  /** Names of the steps, for a multi-step form. Adds the segments, step name and Previous / Next. */
  steps?: string[]
  /**
   * Checks the current step before moving on (Next) or finishing (Finish).
   * Return false to stay, e.g. after showing field errors. Can be async, e.g.
   * to save on the last step; the button shows a spinner meanwhile.
   */
  validateStep?: (step: number) => boolean | Promise<boolean>
  /** Text of the last step's button. */
  finishLabel?: string
  /** Extra classes for the parts. */
  ui?: {
    overlay?: HTMLAttributes['class']
    content?: HTMLAttributes['class']
    header?: HTMLAttributes['class']
    title?: HTMLAttributes['class']
    body?: HTMLAttributes['class']
    footer?: HTMLAttributes['class']
  }
}

const emit = defineEmits<{
  'close:prevent': []
  /** Finish was pressed on the last step and `validateStep` passed. */
  finish: []
}>()

defineSlots<{
  default?(): unknown
  header?(props: { close: () => void }): unknown
  title?(): unknown
  description?(): unknown
  body?(props: { close: () => void; step: number }): unknown
  footer?(props: { close: () => void }): unknown
  content?(props: { close: () => void }): unknown
}>()

/** Whether it is open. Optional: use v-model:open to control it. */
const openModel = defineModel<boolean | undefined>('open', { default: undefined })

/** Current step, from 0. Optional: use v-model:step to control it. Back to 0 each time it opens. */
const step = defineModel<number>('step', { default: 0 })
```
