# Light Box

LightBox is a component designed to display an array of images to the user. A
user clicks on a control and is shown a carousel style presentation of the
defined images.

## Design & usage guidelines

The LightBox's primary goal is to allow users to see a high resolution view of
images contained within the page they're viewing. The application of a LightBox
facilitates smaller thumbnail images to be used in an interface without
compromising the detail within.

The LightBox should only be used when displaying an image at a greater scale is
the primary function of opening it. Additional content types would be better
suited by using a dialogue or alternative method.


## Configuration

### OnRequestClose

This function receives the index of the last image the user viewed before
closing the LightBox with the `lastPosition` key. This can be useful for
returning information about the final state when closed (e.g. for analytics).

### Box Sizing

The lightbox uses the `boxSizing` prop to ensure that thumbnails are rendered in
the correct box-sizing context. This is a solution for a problem where Tailwind
was setting `box-sizing` to `border-box` and causing layout issues.

This is a one-off solution. If this issue is encountered again in other
components, a more robust solution should be considered.

## Component customization

### Composable usage

LightBox exposes its internal building blocks as subcomponents:
`LightBox.Provider`, `LightBox.Content`, `LightBox.Background`,
`LightBox.Overlay`, `LightBox.Toolbar`, `LightBox.Slides`,
`LightBox.Navigation`, `LightBox.Caption`, and `LightBox.Thumbnails`. This gives
you more control over the LightBox's appearance and behaviour.

The thumbnail strip is itself composed of smaller subcomponents you can use
directly: `LightBox.ThumbnailBar`, `LightBox.Thumbnail`, and
`LightBox.ThumbnailImage`. See [Custom thumbnails](#custom-thumbnails) below.

Here's a basic example of how the non-composable LightBox is used:

```tsx
<LightBox
  open={isOpen}
  images={images}
  imageIndex={imageIndex}
  onRequestClose={({ lastPosition }) => {
    setIsOpen(false);
    setImageIndex(lastPosition);
  }}
/>
```

Using LightBox's built-in subcomponents, this UI can alternatively be expressed
as:

```tsx
<LightBox.Provider
  open={isOpen}
  images={images}
  imageIndex={imageIndex}
  onRequestClose={({ lastPosition }) => {
    setIsOpen(false);
    setImageIndex(lastPosition);
  }}
>
  <LightBox.Content />
</LightBox.Provider>
```

If you want to omit specific parts of the LightBox, you can render only the
subcomponents you need. For example, showing the slides and navigation without a
caption or background:

```tsx
<LightBox.Provider open={isOpen} images={images} onRequestClose={onClose}>
  <LightBox.Overlay />
  <LightBox.Toolbar />
  <LightBox.Slides />
  <LightBox.Navigation />
  <LightBox.Thumbnails />
</LightBox.Provider>
```

If you want to build an always-visible inline gallery, with no fullscreen
overlay or close button, you can omit `LightBox.Content` entirely and compose
only the pieces you need. Mouse-move tracking is wired up inside
`LightBox.Content`, so you'll need to forward `handleMouseMove` from
`useLightBoxContext` onto your own container so the navigation arrows appear on
hover:

```tsx
import { LightBox, useLightBoxContext } from "@jobber/components";

function GalleryView() {
  const { handleMouseMove } = useLightBoxContext();

  return (
    <div
      onMouseMove={handleMouseMove}
      style={{ position: "relative", height: 400 }}
    >
      <LightBox.Slides />
      <LightBox.Navigation />
    </div>
  );
}

function ImageGallery() {
  const [index, setIndex] = useState(0);

  return (
    <LightBox.Provider
      open={true}
      images={images}
      imageIndex={index}
      onImageChange={setIndex}
    >
      <GalleryView />
      <LightBox.Thumbnails />
    </LightBox.Provider>
  );
}
```

For any custom component rendered inside `LightBox.Provider`, you can call
`useLightBoxContext` to read state or trigger navigation:

```tsx
import { useLightBoxContext } from "@jobber/components";

function CustomControls() {
  const {
    images,
    currentImageIndex,
    debouncedHandleNext,
    debouncedHandlePrevious,
    handleRequestClose,
  } = useLightBoxContext();

  return (
    <div>
      <span>
        {currentImageIndex + 1} of {images.length}
      </span>
      <button onClick={debouncedHandlePrevious}>Prev</button>
      <button onClick={debouncedHandleNext}>Next</button>
      <button onClick={handleRequestClose}>Close</button>
    </div>
  );
}
```

### Custom thumbnails

`LightBox.Thumbnails` renders the default thumbnail strip and requires no
configuration. When you need to control how each thumbnail renders, compose the
thumbnail subcomponents yourself:

```tsx
import { LightBox, useLightBoxContext } from "@jobber/components";

function CustomThumbnails() {
  const { images } = useLightBoxContext();

  return (
    <LightBox.ThumbnailBar>
      {images.map((image, index) => (
        <LightBox.Thumbnail key={index} index={index}>
          <LightBox.ThumbnailImage
            src={image.url}
            alt={image.alt ?? image.title ?? ""}
          />
        </LightBox.Thumbnail>
      ))}
    </LightBox.ThumbnailBar>
  );
}
```

## Testing

When using Jest to test LightBox implementations, you will need to include this
mock in your test setup:

```js
const scrollIntoViewMock = jest.fn();
window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
```


## Props

### Web

#### LightBox

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `images` | `PresentedImage[]` | Yes | — | Images is an array of objects defining a LightBox image. This object consists of `title`, `caption`, `alt` and `url`.... |
| `onRequestClose` | `(options: RequestCloseOptions) => void` | Yes | — | This function must set open to false in order to close the lightbox. Note there is a 300ms easing animation on lightb... |
| `open` | `boolean` | Yes | — | Specify if the Lightbox is open or closed. |
| `boxSizing` | `BoxSizing` | No | `"content-box"` | Sets the box-sizing for the thumbnails in the lightbox. This is a solution for a problem where tailwind was setting t... |
| `imageIndex` | `number` | No | — | Use this to specify which image in `images` to initialize the lightbox with. This is useful when you have a collectio... |

#### LightBox.Background

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `className` | `string` | No | — |  |

#### LightBox.Navigation

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `nextButtonClassName` | `string` | No | — | The class name to apply to the next button wrapper. |
| `prevButtonClassName` | `string` | No | — | The class name to apply to the previous button wrapper. |

#### LightBox.OpenInNewTabAction

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `ariaLabel` | `string` | No | `"Open image in new tab"` | Accessible label for the action, also used as the tooltip message. |

#### LightBox.Overlay

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `className` | `string` | No | — |  |

#### LightBox.Provider

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `images` | `PresentedImage[]` | Yes | — | Images is an array of objects defining a LightBox image. This object consists of `title`, `caption`, `alt` and `url`.... |
| `boxSizing` | `BoxSizing` | No | `content-box` | Sets the box-sizing for the thumbnails in the lightbox. This is a solution for a problem where tailwind was setting t... |
| `imageIndex` | `number` | No | `0` | Use this to specify which image in `images` to initialize the lightbox with. This is useful when you have a collectio... |
| `onImageChange` | `(index: number) => void` | No | — | Callback function that is invoked whenever the current image index changes. This includes when the user navigates to ... |
| `onRequestClose` | `(options: RequestCloseOptions) => void` | No | — | This function must set open to false in order to close the lightbox. Note there is a 300ms easing animation on lightb... |
| `open` | `boolean` | No | `true` | Specify if the Lightbox is open or closed. |

#### LightBox.Slides

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `className` | `string` | No | — |  |

#### LightBox.Thumbnail

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` | Yes | — | The content rendered inside the selectable thumbnail cell, typically a `LightBox.ThumbnailImage`. |
| `index` | `number` | Yes | — | The index of the image this thumbnail represents. Used to derive the selected state and to navigate to the image when... |
| `className` | `string` | No | — | Additional class name to apply to the thumbnail cell. |

#### LightBox.ThumbnailBar

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `children` | `ReactNode` | Yes | — | The thumbnails to render inside the scrollable bar, typically one `LightBox.Thumbnail` per image. |
| `className` | `string` | No | — | Additional class name to apply to the thumbnail bar wrapper. |

#### LightBox.ThumbnailImage

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `alt` | `string` | Yes | — | The alternative text for the image. |
| `src` | `string` | Yes | — | The image source to render. |
| `className` | `string` | No | — | Additional class name to apply to the image. |
