# 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.

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>
  );
}
```

## 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.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.Background

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

#### LightBox.Overlay

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

#### 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.Slides

| 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. |
