# xpl-spotlight

`xpl-spotlight` renders a guided-tour callout with an anchor dot, optional media, step messaging, and prop-driven footer actions. It can either render inline using the requested `anchorPosition` or attach to a target element with Floating UI when `target` is provided.

## Usage Examples

Use the built-in title/body props and default actions for a simple tour step:

```html
<xpl-spotlight
  heading="Review recent activity"
  body="Check status updates here before moving to the next step."
  action-layout="default"
  anchor-position="top-right"
  current-step="2"
  total-steps="5"
  is-dismissible="true"
  is-open="true"
  primary-action-label="Next"
  secondary-action-label="Back"
  show-actions="true"
  show-image="true"
  show-steps="true"
  target=""
></xpl-spotlight>
```

Anchor the spotlight to a specific element and provide custom media content:

```html
<button id="dashboard-filter-button" type="button">Filters</button>

<xpl-spotlight
  target="#dashboard-filter-button"
  heading="Narrow the results"
  body="Use filters to focus on the accounts or activity that matter most."
  current-step="1"
  total-steps="3"
  primary-action-label="Next"
  secondary-action-label="Skip"
>
  <div slot="image">
    <img src="/assets/spotlight-filter-preview.png" alt="Preview of filter controls" />
  </div>
</xpl-spotlight>
```


Control visibility from the parent with `is-open` and close it by responding to `dismiss`:

```html
<xpl-spotlight
  id="guided-tour-step"
  is-open="true"
  heading="Review recent activity"
  body="Check status updates here before moving to the next step."
></xpl-spotlight>

<script>
  const spotlight = document.getElementById('guided-tour-step');

  spotlight?.addEventListener('dismiss', () => {
    spotlight.setAttribute('is-open', 'false');
  });
</script>
```

Mirror the same layout in plain HTML when documenting or prototyping the component structure outside the custom element:

```html
<div class="xpl-spotlight xpl-spotlight--top-left xpl-spotlight--visible"
     role="dialog" aria-modal="false" aria-labelledby="spotlight-title" tabindex="-1">
  <button type="button" class="xpl-spotlight__target" aria-label="Spotlight indicator">
    <div class="xpl-spotlight__target-outer"></div>
    <div class="xpl-spotlight__target-inner"></div>
  </button>
  <div class="xpl-spotlight__dismiss">
    <xpl-button variant="secondary" size="sm" icon-only aria-label="Dismiss tour step">
      <xpl-icon icon="xmark" size="16"></xpl-icon>
    </xpl-button>
  </div>
  <div class="xpl-spotlight__frame">
    <div class="xpl-spotlight__image-container">
      <img src="/assets/spotlight-preview.png" alt="Feature preview" />
    </div>
    <div class="xpl-spotlight__content">
      <div class="xpl-spotlight__header">
        <span class="xpl-spotlight__title" id="spotlight-title">Tour Title</span>
        <p class="xpl-spotlight__body">Copy about the feature and/or functionality the dot is pointing to.</p>
      </div>
      <div class="xpl-spotlight__actions xpl-spotlight__actions--dual xpl-spotlight__actions--with-steps">
        <div class="xpl-spotlight__action-slot xpl-spotlight__action-slot--secondary">
          <xpl-button variant="secondary">Back</xpl-button>
        </div>
        <span class="xpl-spotlight__step-text">2 of 5</span>
        <div class="xpl-spotlight__action-slot xpl-spotlight__action-slot--primary">
          <xpl-button variant="primary">Next</xpl-button>
        </div>
      </div>
    </div>
  </div>
</div>
```

## Accessibility

`xpl-spotlight` renders with `role="dialog"` and `aria-modal="false"` so assistive technologies treat it as a **non-modal** tour callout: focus is not trapped inside the card, and users can tab to the rest of the page while the step is open. The accessible name comes from the title region:

- When the default title is used, the generated heading element receives the internal `aria-labelledby` id.
- When a custom `title` slot is provided, the component assigns that same id to the first slotted title element if it does not already have one. If the slotted element already has an `id`, that value is kept and `aria-labelledby` references it (the host does not point at a missing id).
- When neither a `heading` prop nor a `title` slot is provided, the dialog falls back to `aria-label="Tour step"` so that assistive technologies always have an accessible name.

### Keyboard navigation

| Key | Behavior |
|-----|----------|
| `Tab` / `Shift+Tab` | Moves focus between interactive controls (dismiss button, secondary action, primary action). Focus is **not** trapped — users can tab out of the spotlight to the rest of the page. |
| `Escape` | Emits `dismiss` when `isDismissible` is true and focus is inside the spotlight. Does nothing when focus is outside the component or the spotlight is closed. |
| `Enter` / `Space` | Activates the focused button (dismiss, primary, or secondary action) via standard `xpl-button` behavior. |

### Focus management

- **On open:** focus moves automatically to the dismiss button (if `isDismissible` is true) or the first action button. When neither a dismiss button nor action buttons are rendered, the host element itself receives focus via `tabindex="-1"`.
- **On close:** focus returns to the element that was focused before the spotlight opened, provided that element is still in the DOM.
- The decorative anchor dot (`.xpl-spotlight__target`) is marked `aria-hidden="true"` so it is not announced or focusable.


## Design Tokens

The component relies on Foundation purpose tokens in `spotlight.css` so light/dark mode works without component-level `.dark` overrides.

| Property | Token |
|----------|-------|
| Card width | `var(--xpl-spotlight-width)` on the host (defined in `spotlight.css`; design-specific width until shared layout tokens exist — see `TECH_DEBT.md`) |
| Image region min-height | `var(--xpl-spotlight-image-min-height)` on `.xpl-spotlight__image-container` (same note as card width) |
| Card background | `var(--xpl-background-surface-default)` |
| Host stacking | `var(--xpl-z-index-800)` |
| Placeholder pattern | `var(--xpl-background-surface-subtle)` |
| Heading text | `var(--xpl-text-default)` |
| Body text | `var(--xpl-text-default)` |
| Step text | `var(--xpl-text-subdued)` |
| Open/closed visibility | `opacity` transition on `.xpl-spotlight--visible` |
| Anchor outer ring | `var(--xpl-background-surface-information-bold)` |
| Anchor inner dot | `var(--xpl-background-accent-default)` |
| Image placeholder icon/text | `var(--xpl-icon-subtle)` |
| Focus outline | `var(--xpl-border-focus)` |
| Anchored host (when `target` is set) | `position: fixed` via `.xpl-spotlight--anchored`; `left` / `top` set from Floating UI (`strategy: 'fixed'`) |

## Icon Mapping

`xpl-spotlight` uses Apollo icons for fixed controls and fallback media placeholders:

| UI area | `xpl-icon` name |
|---------|------------------|
| Dismiss button | `xmark` |
| Empty image placeholder, image affordance | `photo` |
| Empty image placeholder, video affordance | `video` |

## Deprecated props

None.

## Notes

- `showImage="false"` hides the media region even if `slot="image"` content is present.
- When `showImage` is left enabled and no `slot="image"` content is provided, the component renders the built-in placeholder instead of collapsing the media region.
- `showActions="false"` removes the default actions row entirely.
- `is-open="false"` keeps the host element mounted but renders no spotlight UI until it is opened again.
- `action-layout` defaults to `"default"`. `action-layout="default"` renders the Back/Next pair with **disabled** edge buttons at step boundaries (Back disabled on step 1, Next disabled on the last step). `action-layout="dual"` renders the Back/Next pair with **hidden** edge buttons at step boundaries. `action-layout="single"` renders a single primary action.
- `primary-action-label` and `secondary-action-label` control the default button text without needing action slots.
- `target` enables Floating UI positioning with `offset`, `flip`, `shift`, and `autoUpdate` so the spotlight follows the referenced element on resize and scroll. Default offset and shift padding match Foundation spacing (`--xpl-space-12` / `--xpl-space-8`).
- If `target` is missing or contains an invalid CSS selector, the component safely falls back to the requested `anchor-position` instead of throwing.
- **Storybook:** open **Components → Spotlight** to adjust props via controls. Set `target` to `#spotlight-story-anchor` to preview Floating UI anchoring against the demo button on the canvas. The **Design** addon links to the Apollo Web Figma file (replace the `node-id` in Storybook with the Spotlight frame when Design publishes a dedicated link).

<!-- Auto Generated Below -->


## Properties

| Property               | Attribute                | Description                                                                                                                                                                                                                                                                                                                                                    | Type                                                           | Default      |
| ---------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------------ |
| `actionLayout`         | `action-layout`          | Footer action layout. `default` and `dual` both render the standard Back/Next layout, while `single` renders a single acknowledgement CTA (e.g. "Got it!"). Only applies when `showActions` is `true`.                                                                                                                                                         | `"default" \| "dual" \| "single"`                              | `'default'`  |
| `anchorPosition`       | `anchor-position`        | Which corner of the card the anchor dot uses when not using `target`, or as the preferred side for Floating UI.                                                                                                                                                                                                                                                | `"bottom-left" \| "bottom-right" \| "top-left" \| "top-right"` | `'top-left'` |
| `body`                 | `body`                   | Body copy when the `body` slot has no slotted content. Whitespace-only values are treated as empty (no fallback paragraph).                                                                                                                                                                                                                                    | `string`                                                       | `undefined`  |
| `currentStep`          | `current-step`           | Current step index (1-based). Used with `totalSteps` for the step label and for disabling default Back/Next when `showSteps` is true.                                                                                                                                                                                                                          | `number`                                                       | `1`          |
| `heading`              | `heading`                | Heading when the `title` slot has no slotted content. Whitespace-only values are treated as empty (no fallback title span).                                                                                                                                                                                                                                    | `string`                                                       | `undefined`  |
| `isDismissible`        | `is-dismissible`         | Whether to show the dismiss (close) control.                                                                                                                                                                                                                                                                                                                   | `boolean`                                                      | `true`       |
| `isOpen`               | `is-open`                | Controls whether the spotlight is rendered. When false, the host remains in the DOM but the spotlight UI is hidden.                                                                                                                                                                                                                                            | `boolean`                                                      | `true`       |
| `primaryActionLabel`   | `primary-action-label`   | Label used for the default primary action button. Only applies when `showActions` is `true`.                                                                                                                                                                                                                                                                   | `string`                                                       | `'Next'`     |
| `secondaryActionLabel` | `secondary-action-label` | Label used for the default secondary action button. Only applies when `showActions` is `true`.                                                                                                                                                                                                                                                                 | `string`                                                       | `'Back'`     |
| `showActions`          | `show-actions`           | Whether to show the actions row (primary / secondary and optional step counter). When `false`, the spotlight has no action buttons — useful for single-step informational callouts that rely solely on the dismiss control.                                                                                                                                    | `boolean`                                                      | `true`       |
| `showImage`            | `show-image`             | When true, the media region is shown. Slotted `image` content is rendered when present; otherwise the built-in placeholder UI is shown. When false, the entire media block is hidden.                                                                                                                                                                          | `boolean`                                                      | `true`       |
| `showSteps`            | `show-steps`             | Whether to show the step counter between actions (`currentStep` of `totalSteps`) and whether default Back/Next use step-based disabled state.                                                                                                                                                                                                                  | `boolean`                                                      | `true`       |
| `target`               | `target`                 | CSS selector for the element this spotlight anchors to. When set, placement uses Floating UI (`computePosition` with `strategy: 'fixed'`, plus `flip`, `shift`, `offset`) and `autoUpdate` for scroll/resize. The host receives `left` / `top` and the `xpl-spotlight--anchored` class. Invalid selectors are ignored; layout falls back to `anchor-position`. | `string`                                                       | `undefined`  |
| `totalSteps`           | `total-steps`            | Total number of steps (1-based upper bound). Used with `currentStep` for the step label and for disabling default Back/Next when `showSteps` is true.                                                                                                                                                                                                          | `number`                                                       | `1`          |


## Events

| Event             | Description                                                                                    | Type                |
| ----------------- | ---------------------------------------------------------------------------------------------- | ------------------- |
| `dismiss`         | Emitted when the dismiss control is activated or Escape is pressed (when dismissible).         | `CustomEvent<void>` |
| `primaryAction`   | Emitted when the primary action control is activated (default: Next).                          | `CustomEvent<void>` |
| `secondaryAction` | Emitted when the secondary action control is activated (default: Back).                        | `CustomEvent<void>` |
| `targetClick`     | Emitted when the target indicator is clicked. Consumers typically toggle `isOpen` in response. | `CustomEvent<void>` |


## Slots

| Slot      | Description                                                                                                    |
| --------- | -------------------------------------------------------------------------------------------------------------- |
| `"body"`  | Main description; falls back to the `body` prop when the slot is empty and `body` has non-whitespace text      |
| `"image"` | Optional media slot. When empty and `showImage` is true, the component renders a built-in placeholder instead. |
| `"title"` | Step title; falls back to the `heading` prop when the slot is empty and `heading` has non-whitespace text      |


## Dependencies

### Depends on

- [xpl-button](../xpl-button)
- [xpl-icon](../xpl-icon)

### Graph
```mermaid
graph TD;
  xpl-spotlight --> xpl-button
  xpl-spotlight --> xpl-icon
  xpl-button --> xpl-icon
  style xpl-spotlight fill:#f9f,stroke:#333,stroke-width:4px
```

----------------------------------------------

*Built with [StencilJS](https://stenciljs.com/)*
