Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,20 @@ The component works out of the box in React Server Components environments (e.g.

## Props

| Prop | Type | Description |
| ------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `image` | `string` (required) | Image URL or base64 data URL to edit. |
| `options` | `ImageEditorOptions` | Editor configuration: `projectId`, `user`, `features`, `theme`, `locale`, `translations`, `env`, `offline`, `licenseUrl`, `defaultPrompt`, `autoSubmitPrompt`, `aiAssistantOpenState`. |
| `editorId` | `string` | id for the container div. Cosmetic — the editor mounts by element reference. |
| `minHeight` | `number \| string` | Minimum height of the editor container. Defaults to `500`. |
| `style` | `CSSProperties` | Styles applied to the container div. |
| `onLoad` | `(editor) => void` | Called with the editor instance once it is mounted. |
| `onSave` | `({ dataUrl, blob }) => void` | Called when the user saves the edited image. |
| `onCancel` | `() => void` | Called when the user cancels editing. |
| `onLoadError` | `() => void` | Called when the image fails to load into the canvas (CORS, 404, decode error). |
| `onError` | `(error: Error) => void` | Wrapper-level failures: embed script load, editor creation, or image reset. Falls back to `console.error` when absent. |
| Prop | Type | Description |
| -------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `image` | `string` (required) | Image URL or base64 data URL to edit. |
| `options` | `ImageEditorOptions` | Editor configuration: `projectId`, `user`, `features`, `theme`, `locale`, `translations`, `env`, `offline`, `licenseUrl`, `defaultPrompt`, `autoSubmitPrompt`, `aiAssistantOpenState`. |
| `editorId` | `string` | id for the container div. Cosmetic — the editor mounts by element reference. |
| `minHeight` | `number \| string` | Minimum height of the editor container. Defaults to `500`. |
| `style` | `CSSProperties` | Styles applied to the container div. Overrides the default `flex: 1`. |
| `wrapperStyle` | `CSSProperties` | Styles applied to the outer wrapper div, which owns `minHeight` and the flex layout. Set this to drop the editor into a non-flex layout. |
| `ariaLabel` | `string` | Accessible name for the editor region. Defaults to `'Image editor'`. |
| `onLoad` | `(editor) => void` | Called with the editor instance once it is mounted. |
| `onSave` | `({ dataUrl, blob }) => void` | Called when the user saves the edited image. |
| `onCancel` | `() => void` | Called when the user cancels editing. |
| `onLoadError` | `() => void` | Called when the image fails to load into the canvas (CORS, 404, decode error). |
| `onError` | `(error: Error) => void` | Wrapper-level failures: embed script load, editor creation, or image reset. Falls back to `console.error` when absent. |

## Editor instance (ref)

Expand Down
23 changes: 21 additions & 2 deletions src/ImageEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@ function ImageEditorInner(
props: ImageEditorProps,
ref: React.Ref<ImageEditorRef>
) {
const { image, options = {}, scriptUrl, minHeight = 500, style = {} } = props;
const {
image,
options = {},
scriptUrl,
minHeight = 500,
style = {},
wrapperStyle = {},
ariaLabel = 'Image editor',
} = props;

const [editor, setEditor] = useState<ImageEditorInstance | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -196,9 +204,20 @@ function ImageEditorInner(
flex: 1,
display: 'flex',
minHeight: minHeight,
...wrapperStyle,
}}
>
<div id={editorId} ref={containerRef} style={{ ...style, flex: 1 }} />
<div
id={editorId}
ref={containerRef}
// The embed renders an unlabelled generic div inside, so without a
// role and a name the whole editing surface is anonymous to
// assistive tech.
role="region"
aria-label={ariaLabel}
// flex first: a default the consumer's style can override.
style={{ flex: 1, ...style }}
/>
</div>
);
}
Expand Down
12 changes: 11 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,18 @@ export interface ImageEditorProps {
editorId?: string;
/** Minimum height of the editor container. Defaults to 500. */
minHeight?: number | string;
/** Styles applied to the container div. */
/** Styles applied to the container div. Overrides the default `flex: 1`. */
style?: CSSProperties;
/**
* Styles applied to the outer wrapper div, which owns `minHeight` and the
* flex layout. Set this to drop the editor into a non-flex layout.
*/
wrapperStyle?: CSSProperties;
/**
* Accessible name for the editor region. The embed renders no landmark
* and no name of its own. Defaults to 'Image editor'.
*/
ariaLabel?: string;
/**
* Override the embed script URL (e.g. to pin an environment). One embed
* per page: the first loader to run installs window.ImageEditor and wins
Expand Down
39 changes: 39 additions & 0 deletions test/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,45 @@ it('applies minHeight and style to the container', async () => {
);
});

it('lets style override the container default and wrapperStyle reach the wrapper', async () => {
render(
<ImageEditor
editorId="styled"
image="img-a"
minHeight={300}
style={{ flex: 'none', width: 640 }}
wrapperStyle={{ display: 'block', minHeight: 0 }}
/>
);
await flush();

const container = document.querySelector('#styled') as HTMLElement;
const wrapper = container.parentElement as HTMLElement;

// flex is a default now, not a hardcoded override. ('none' is stored in
// expanded longhand form; the default `flex: 1` would be '1 1 0%'.)
expect(container.style.flex).toBe('0 0 auto');
expect(container.style.width).toBe('640px');
// The outer wrapper is reachable, so the component can be dropped into a
// non-flex layout.
expect(wrapper.style.display).toBe('block');
expect(wrapper.style.minHeight).toBe('0px');
});

it('gives the editor region an accessible name, overridable via ariaLabel', async () => {
const { rerender } = render(<ImageEditor editorId="a11y" image="img-a" />);
await flush();

const container = document.querySelector('#a11y') as HTMLElement;
expect(container.getAttribute('role')).toBe('region');
expect(container.getAttribute('aria-label')).toBe('Image editor');

rerender(
<ImageEditor editorId="a11y" image="img-a" ariaLabel="Avatar cropper" />
);
expect(container.getAttribute('aria-label')).toBe('Avatar cropper');
});

it('passes onCancel and onLoadError through to the editor', async () => {
const onCancel = vi.fn();
const onLoadError = vi.fn();
Expand Down