Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Added new image callbacks to RichText components to make it easier to use images #894

Merged
merged 7 commits into from
Dec 5, 2024
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
Loading