Skip to content

Commit

Permalink
Merge pull request #894 from equinor/feat/rich-text-imgs
Browse files Browse the repository at this point in the history
✨ Added new image callbacks to RichText components to make it easier to use images
  • Loading branch information
mariush2 authored Dec 5, 2024
2 parents e88a9e3 + a2c7401 commit 26ca295
Show file tree
Hide file tree
Showing 15 changed files with 360 additions and 53 deletions.
10 changes: 5 additions & 5 deletions src/atoms/hooks/useAmplifyKit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ import { useMemo } from 'react';

import { RichText } from 'src/molecules/RichTextEditor';
import {
OnImageUploadFn,
ImageExtensionFnProps,
RichTextEditorFeatures,
} from 'src/molecules/RichTextEditor/RichTextEditor.types';

interface AmplifyKitProps {
interface AmplifyKitProps extends ImageExtensionFnProps {
features?: RichTextEditorFeatures[];
placeholder?: string;
onImageUpload?: OnImageUploadFn;
}

/* c8 ignore start */
export const useAmplifyKit = ({
features,
placeholder,
onImageUpload,
onImageRead,
}: AmplifyKitProps) => {
// This hooks is where we can use the features API we made to turn off extensions in the new configure API
// Currently its only turning off the image extension since its the only one that needs to be turned off for the moment
Expand All @@ -40,13 +40,13 @@ export const useAmplifyKit = ({
? {}
: false,
image: features?.includes(RichTextEditorFeatures.IMAGES)
? { onImageUpload }
? { onImageUpload, onImageRead }
: false,
heading: features?.includes(RichTextEditorFeatures.HEADERS)
? {}
: false,
}),
[onImageUpload, placeholder, features]
[placeholder, features, onImageUpload, onImageRead]
);
};
/* c8 ignore stop */
23 changes: 23 additions & 0 deletions src/atoms/hooks/useRichTextImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useQuery } from '@tanstack/react-query';

import { ImageExtensionFnProps } from 'src/molecules/RichTextEditor/RichTextEditor.types';

export function useRichTextImage(
image: string,
onImageRead: ImageExtensionFnProps['onImageRead'] | undefined
) {
return useQuery({
queryKey: [image],
queryFn: async () => {
if (onImageRead) {
const response = await onImageRead(image);
const extension = image.split('.').at(-1);
return `data:image/${extension};base64,${response}`;
}

return image;
},
staleTime: Infinity,
gcTime: Infinity,
});
}
76 changes: 76 additions & 0 deletions src/atoms/style/lightTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,81 @@ export const lightTokens = css`
--amplify_dataviz_lightgray_default: rgba(144, 155, 162, 1);
--amplify_dataviz_lightgray_darker: rgba(91, 102, 108, 1);
--amplify_dataviz_lightgray_lighter: rgba(194, 200, 204, 1);
--eds_text__static_icons__default: rgba(61, 61, 61, 1);
--eds_text__static_icons__secondary: rgba(86, 86, 86, 1);
--eds_text__static_icons__tertiary: rgba(111, 111, 111, 1);
--eds_text__static_icons__primary_white: rgba(255, 255, 255, 1);
--eds_ui_background__default: rgba(255, 255, 255, 1);
--eds_ui_background__semitransparent: rgba(255, 255, 255, 0.2);
--eds_ui_background__light: rgba(247, 247, 247, 1);
--eds_ui_background__scrim: rgba(0, 0, 0, 0.4);
--eds_ui_background__overlay: rgba(0, 0, 0, 0.8);
--eds_ui_background__medium: rgba(220, 220, 220, 1);
--eds_ui_background__info: rgba(213, 234, 244, 1);
--eds_ui_background__warning: rgba(255, 231, 214, 1);
--eds_ui_background__danger: rgba(255, 193, 193, 1);
--eds_infographic_substitute__purple_berry: rgba(140, 17, 89, 1);
--eds_infographic_substitute__pink_rose: rgba(226, 73, 115, 1);
--eds_infographic_substitute__pink_salmon: rgba(255, 146, 168, 1);
--eds_infographic_substitute__green_cucumber: rgba(0, 95, 87, 1);
--eds_infographic_substitute__green_succulent: rgba(0, 151, 123, 1);
--eds_infographic_substitute__green_mint: rgba(64, 211, 143, 1);
--eds_infographic_substitute__blue_ocean: rgba(0, 64, 136, 1);
--eds_infographic_substitute__blue_overcast: rgba(0, 132, 196, 1);
--eds_infographic_substitute__blue_sky: rgba(82, 192, 255, 1);
--eds_infographic_primary__moss_green_100: rgba(0, 112, 121, 1);
--eds_infographic_primary__moss_green_55: rgba(115, 177, 181, 1);
--eds_infographic_primary__moss_green_34: rgba(168, 206, 209, 1);
--eds_infographic_primary__moss_green_21: rgba(201, 224, 226, 1);
--eds_infographic_primary__moss_green_13: rgba(222, 237, 238, 1);
--eds_infographic_primary__energy_red_100: rgba(235, 0, 55, 1);
--eds_infographic_primary__energy_red_55: rgba(255, 125, 152, 1);
--eds_infographic_primary__energy_red_34: rgba(255, 174, 191, 1);
--eds_infographic_primary__energy_red_21: rgba(255, 205, 215, 1);
--eds_infographic_primary__energy_red_13: rgba(255, 224, 231, 1);
--eds_infographic_primary__weathered_red: rgba(125, 0, 35, 1);
--eds_infographic_primary__slate_blue: rgba(36, 55, 70, 1);
--eds_infographic_primary__spruce_wood: rgba(255, 231, 214, 1);
--eds_infographic_primary__mist_blue: rgba(213, 234, 244, 1);
--eds_infographic_primary__lichen_green: rgba(230, 250, 236, 1);
--eds_logo__fill_positive: rgba(235, 0, 55, 1);
--eds_logo__fill_negative: rgba(255, 255, 255, 1);
--eds_interactive_primary__selected_highlight: rgba(230, 250, 236, 1);
--eds_interactive_primary__selected_hover: rgba(195, 243, 210, 1);
--eds_interactive_primary__resting: rgba(0, 112, 121, 1);
--eds_interactive_primary__hover: rgba(0, 79, 85, 1);
--eds_interactive_primary__hover_alt: rgba(222, 237, 238, 1);
--eds_interactive_secondary__highlight: rgba(213, 234, 244, 1);
--eds_interactive_secondary__resting: rgba(36, 55, 70, 1);
--eds_interactive_secondary__link_hover: rgba(23, 36, 47, 1);
--eds_interactive_danger__highlight: rgba(255, 193, 193, 1);
--eds_interactive_danger__resting: rgba(235, 0, 0, 1);
--eds_interactive_danger__hover: rgba(179, 13, 47, 1);
--eds_interactive_danger__text: rgba(179, 13, 47, 1);
--eds_interactive_warning__highlight: rgba(255, 231, 214, 1);
--eds_interactive_warning__resting: rgba(255, 146, 0, 1);
--eds_interactive_warning__hover: rgba(173, 98, 0, 1);
--eds_interactive_warning__text: rgba(173, 98, 0, 1);
--eds_interactive_success__highlight: rgba(230, 250, 236, 1);
--eds_interactive_success__resting: rgba(75, 183, 72, 1);
--eds_interactive_success__hover: rgba(53, 129, 50, 1);
--eds_interactive_success__text: rgba(53, 129, 50, 1);
--eds_interactive_table__cell__fill_resting: rgba(255, 255, 255, 1);
--eds_interactive_table__cell__fill_hover: rgba(234, 234, 234, 1);
--eds_interactive_table__cell__fill_activated: rgba(230, 250, 236, 1);
--eds_interactive_table__header__fill_activated: rgba(234, 234, 234, 1);
--eds_interactive_table__header__fill_hover: rgba(220, 220, 220, 1);
--eds_interactive_table__header__fill_resting: rgba(247, 247, 247, 1);
--eds_interactive__disabled__text: rgba(190, 190, 190, 1);
--eds_interactive__text_highlight: rgba(213, 234, 244, 1);
--eds_interactive__focus: rgba(0, 112, 121, 1);
--eds_interactive__disabled__border: rgba(220, 220, 220, 1);
--eds_interactive__disabled__fill: rgba(234, 234, 234, 1);
--eds_interactive__link_on_interactive_colors: rgba(255, 255, 255, 1);
--eds_interactive__icon_on_interactive_colors: rgba(255, 255, 255, 1);
--eds_interactive__link_in_snackbars: rgba(151, 202, 206, 1);
--eds_interactive__pressed_overlay_dark: rgba(0, 0, 0, 0.2);
--eds_interactive__pressed_overlay_light: rgba(255, 255, 255, 0.2);
}
`;
21 changes: 18 additions & 3 deletions src/atoms/utils/richtext.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {
DEFAULT_FEATURES,
OnImageUploadFn,
ImageExtensionFnProps,
RichTextEditorFeatures,
} from 'src/molecules/RichTextEditor/RichTextEditor.types';

export interface FeaturesProps {
export interface FeaturesProps
extends Pick<ImageExtensionFnProps, 'onImageUpload'> {
features?: RichTextEditorFeatures[];
extendFeatures?: RichTextEditorFeatures[];
removeFeatures?: RichTextEditorFeatures[];
onImageUpload?: OnImageUploadFn;
}

export function getFeatures({
Expand All @@ -35,9 +35,18 @@ export function getFeatures({
return usedFeatures;
}

/**
* @deprecated - Use the onImageRead prop instead of tokens
*/
export const IMG_WITH_SRC_AND_ALT = /(<img src=".*?" alt="(.*?)">)/g;
/**
* @deprecated - Use the onImageRead prop instead of tokens
*/
export const IMG_WITH_ALT = /(<img alt="(.*?)" \/>)/g;

/**
* @deprecated - Use the onImageRead prop instead of tokens
*/
export function extractImageUrls(value: string | undefined): string[] {
if (!value) return [];

Expand All @@ -50,6 +59,9 @@ export function extractImageUrls(value: string | undefined): string[] {
return images;
}

/**
* @deprecated - Use the onImageRead prop instead of tokens
*/
export function imageToB64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
Expand All @@ -61,6 +73,9 @@ export function imageToB64(file: File): Promise<string> {
});
}

/**
* @deprecated - Use the onImageRead prop instead of tokens
*/
export function cleanRichTextValue(value: string) {
return value.replaceAll(IMG_WITH_SRC_AND_ALT, `<img alt="$2" />`);
}
26 changes: 24 additions & 2 deletions src/molecules/RichTextDisplay/RichTextDisplay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { tokens } from '@equinor/eds-tokens';
import { faker } from '@faker-js/faker';

import { RichTextDisplay } from 'src/molecules/RichTextDisplay/RichTextDisplay';
import { act, render, screen } from 'src/tests/test-utils';
import { act, render, renderWithProviders, screen } from 'src/tests/test-utils';

import { expect } from 'vitest';

const { spacings } = tokens;

Expand All @@ -23,7 +25,7 @@ test('Image read token prop works as expected', async () => {
'https://images.unsplash.com/photo-1682687221363-72518513620e';
const randomToken = 'sv=2023-08-03';

render(
renderWithProviders(
<RichTextDisplay
value={`<img src="${randomUrl}"/>`}
imgReadToken={randomToken}
Expand All @@ -41,6 +43,26 @@ test('Image read token prop works as expected', async () => {
);
});

test('onImageRead fn works as expected', async () => {
const randomUrl =
'https://images.unsplash.com/photo-1682687221363-72518513620e';
const handleImageRead = vi.fn();

renderWithProviders(
<RichTextDisplay
value={`<img src="${randomUrl}"/>`}
onImageRead={handleImageRead}
/>
);

// Wait for tip tap to initialize
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
});

expect(handleImageRead).toHaveBeenCalledWith(randomUrl);
});

test('Shows content when sending in new value', async () => {
const content = faker.animal.dog();
const { rerender } = render(<RichTextDisplay value={content} />);
Expand Down
32 changes: 28 additions & 4 deletions src/molecules/RichTextDisplay/RichTextDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,58 @@ import { FC, HTMLAttributes, useEffect, useRef } from 'react';

import { Editor, Extensions, useEditor } from '@tiptap/react';

import { AmplifyKit } from 'src/molecules/RichTextEditor/custom-extensions/AmplifyKit';
import { useAmplifyKit } from 'src/atoms';
import {
EditorContent,
EditorStyling,
} from 'src/molecules/RichTextEditor/RichTextEditor.styles';
import {
DEFAULT_FEATURES,
ImageExtensionFnProps,
} from 'src/molecules/RichTextEditor/RichTextEditor.types';

export interface RichTextDisplayProps {
export interface RichTextDisplayProps
extends Pick<ImageExtensionFnProps, 'onImageRead'> {
value: string | null | undefined;
/**
* @deprecated - Use OnImageRead instead
*/
imgReadToken?: string;
lightBackground?: boolean;
padding?: 'sm' | 'md' | 'lg' | 'none';
extensions?: Extensions;
children?: (editor: Editor) => JSX.Element;
}

/**
*
* @param value - Rich text content
* @param imgReadToken - Deprecated, use onImageRead instead
* @param onImageRead - handler used when loading images, expects b64 string to be returned
* @param lightBackground - if it should have a light BG color
* @param padding - padding in the editor
* @param extensions - additional extensions to be added
* @param children - render prop for custom rendering, provides editor via callback
*/
export const RichTextDisplay: FC<
RichTextDisplayProps & HTMLAttributes<HTMLDivElement>
> = ({
value,
imgReadToken,
onImageRead,
lightBackground = true,
padding = 'md',
extensions = [AmplifyKit],
extensions,
children,
...rest
}) => {
const defaultExtensions = useAmplifyKit({
features: DEFAULT_FEATURES,
onImageRead: onImageRead,
});
const editor = useEditor({
extensions: extensions,
/* c8 ignore next */
extensions: extensions ? extensions : [defaultExtensions],
content: imgReadToken
? value?.replaceAll(/(<img src=")(.+)("\/>)/g, `$1$2?${imgReadToken}$3`)
: value,
Expand Down
Loading

0 comments on commit 26ca295

Please sign in to comment.