Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
fda2780
Image upload component wip
Magnusrm Aug 29, 2025
6d64978
update cropping component
Magnusrm Aug 29, 2025
aefafc0
add circle config
Magnusrm Sep 1, 2025
35af5c2
move eventlisteners to canvas
Magnusrm Sep 1, 2025
7f3fe76
explicit use of constrainToArea
Magnusrm Sep 1, 2025
daf3f37
further refactor image upload component
Magnusrm Sep 1, 2025
12e2bca
add common utils for draw and handlecrop functions + variable viewpor…
lassopicasso Sep 2, 2025
f78e84d
add common utils for draw and handlecrop functions + variable viewpor…
lassopicasso Sep 2, 2025
530b1a1
extract functions to utils (#3663)
Magnusrm Sep 3, 2025
7e6c115
implement cropAsConfig prop into viewport config (#3662)
lassopicasso Sep 3, 2025
741579f
prevent scrolling (#3658)
lassopicasso Sep 3, 2025
d15cc89
Feat/refine image upload buttons according to design (#3660)
lassopicasso Sep 3, 2025
211edd7
Move the image cropper into a card (#3665)
Magnusrm Sep 3, 2025
6602e9c
clean up css and style closer to figma design (#3669)
Magnusrm Sep 4, 2025
a82fe47
added save and cancel buttons with functionality (some left for save …
lassopicasso Sep 5, 2025
c336050
Redesign slider + buttons in img controller (#3671)
lassopicasso Sep 5, 2025
658e9fd
Extract canvas and effects into its own component (#3674)
Magnusrm Sep 5, 2025
bcaf56f
feat/ handle saved image and new display when image is saved (#3675)
lassopicasso Sep 8, 2025
6b65f5c
add check at higher level for stored image (#3680)
lassopicasso Sep 9, 2025
420fc34
zoom to the center of viewport (#3681)
lassopicasso Sep 9, 2025
38a6ada
Feat/custom crop area config + renaming (#3682)
lassopicasso Sep 9, 2025
a115242
Feat/remove-dropzone-icon-and-change-background (#3679)
Magnusrm Sep 9, 2025
1629e7f
fix zooming limits (#3684)
lassopicasso Sep 9, 2025
0d8477f
fix: active pointer canvas and keyboard support on change image (#3683)
lassopicasso Sep 10, 2025
7d33561
update icons
Magnusrm Sep 10, 2025
821e4d1
Add validationmessages for imageupload size and types (#3687)
Magnusrm Sep 10, 2025
805df2f
Feat/use-language-for-component-texts (#3689)
Magnusrm Sep 10, 2025
24da4ac
Display uploaded image+more (#3690)
lassopicasso Sep 10, 2025
5976437
refactor: calculations (#3697)
lassopicasso Sep 11, 2025
fbb9514
feat: Support config (#3700)
lassopicasso Sep 12, 2025
38db36c
small adjustment
lassopicasso Sep 16, 2025
b57f83c
change config from one object to 3 props
lassopicasso Sep 16, 2025
acedcd9
Merge remote-tracking branch 'origin/main' into feat/image-upload-com…
Magnusrm Sep 17, 2025
2484b05
New dropzone using the new figma design (#3708)
Magnusrm Sep 17, 2025
abb0710
clean up comment, fix text-alignment
Magnusrm Sep 17, 2025
97d9977
remove redundant css class
Magnusrm Sep 17, 2025
ed86243
fix cropped preview
Magnusrm Sep 17, 2025
26b53c9
feat: Handle canvas size based on grid or mobile viewport and use fil…
lassopicasso Sep 17, 2025
1681b0a
add unit tests and fix validation (#3714)
lassopicasso Sep 17, 2025
c0c0cb8
Add displaydata expression test to ImageUpload (#3719)
Magnusrm Sep 18, 2025
0e685d9
adjust to equal height and width if circle and they are uneven
lassopicasso Sep 18, 2025
cfd3a11
fix duplicate ids and use ref instead of getElementById
Magnusrm Sep 18, 2025
ee0aab3
clean up css
Magnusrm Sep 18, 2025
76e1893
fix sizing
lassopicasso Sep 18, 2025
1f948f4
Merge branch 'feat/image-upload-component' of https://github.com/Alti…
lassopicasso Sep 18, 2025
be47527
use unique id, remove preventDefault for dropzone
Magnusrm Sep 18, 2025
753184f
remove a type-casting
Magnusrm Sep 18, 2025
e9aa62a
add accesible title to reset zoom button
Magnusrm Sep 18, 2025
f7958fe
remove redundant css
Magnusrm Sep 18, 2025
0a02587
fix change image button
Magnusrm Sep 18, 2025
6f79243
add cypress tests for image upload component (#3721)
Magnusrm Sep 18, 2025
ae76f44
make filetypes the same
Magnusrm Sep 19, 2025
99a3dc6
make filetypes match
Magnusrm Sep 19, 2025
0c15edf
support summary2 (#3725)
lassopicasso Sep 19, 2025
825d32c
support validFileEndings config
lassopicasso Sep 25, 2025
edf4926
merge
lassopicasso Sep 25, 2025
4065306
replace chess background when image is saved
lassopicasso Sep 25, 2025
715a1b4
fix edge case where canvas gets too low between 1160-1200px when grid…
lassopicasso Sep 26, 2025
4afbe86
simplify responsivness and keep fixed size of the canvas
lassopicasso Sep 26, 2025
dc21ba9
set min zoom as default
lassopicasso Sep 26, 2025
d57d892
remove error message when save
lassopicasso Sep 26, 2025
b6e0b1e
add tests for imageupload in summary2
lassopicasso Sep 29, 2025
e683284
reverse validfileendings config support
lassopicasso Oct 1, 2025
8d6ac3d
add cypress test where replacing image
lassopicasso Oct 1, 2025
3823a85
react recommendations
lassopicasso Oct 1, 2025
0294604
add utility tests
lassopicasso Oct 2, 2025
51d8654
Merge branch 'main' into feat/image-upload-component
lassopicasso Oct 2, 2025
221082c
remove some comments
lassopicasso Oct 2, 2025
20c479c
add specific image types to be allowed
lassopicasso Oct 3, 2025
019ea8f
small adjustment
lassopicasso Oct 6, 2025
d0993c6
new summary of image upload, show image
lassopicasso Oct 6, 2025
8e526e7
some text changes
lassopicasso Oct 7, 2025
5be3187
update tests for new summary of image upload
lassopicasso Oct 7, 2025
057189a
check for possible animation file type and inform user that only firs…
lassopicasso Oct 7, 2025
5f0dbcf
support readOnly config
lassopicasso Oct 7, 2025
1d068cd
fix test
lassopicasso Oct 7, 2025
50f6b68
rabbit feedback
lassopicasso Oct 7, 2025
089711e
rename config from square to rectangle
lassopicasso Oct 9, 2025
1b4d5ee
make useFileUploaderDataBindingsValidation support ImageUpload as well
lassopicasso Oct 9, 2025
f391958
Better error message for imageUpload is not supported in summary comp…
lassopicasso Oct 9, 2025
8812b14
check that allowedContentTypes in app meta data is valid when renderi…
lassopicasso Oct 10, 2025
f87e83b
remove emojis in css comments
lassopicasso Oct 10, 2025
bea697c
move logic out of jsx into a function
lassopicasso Oct 10, 2025
21b54b6
rename css class
lassopicasso Oct 10, 2025
6f05f4f
text resource to aria-label
lassopicasso Oct 10, 2025
2ba154b
move existing canvas files into own folder
lassopicasso Oct 11, 2025
07bce70
refactor image canvas
lassopicasso Oct 11, 2025
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
1 change: 0 additions & 1 deletion src/app-components/Card/Card.module.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
.mediaCard {
padding: 0;
margin-bottom: -7px;
}
.mediaCard img {
object-fit: cover;
Expand Down
6 changes: 6 additions & 0 deletions src/app-components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ type AppCardProps = {
color?: Parameters<typeof Card>[0]['color'];
children?: React.ReactNode;
variant?: 'tinted' | 'default';
className?: string;
ref?: React.Ref<HTMLDivElement>;
};

export function AppCard({
Expand All @@ -24,11 +26,15 @@ export function AppCard({
mediaPosition = 'top',
children,
variant = 'tinted',
className,
ref,
}: AppCardProps) {
return (
<Card
data-color={color}
variant={variant}
className={className}
ref={ref}
>
{media && mediaPosition === 'top' && <Card.Block className={classes.mediaCard}>{media}</Card.Block>}
<Card.Block>
Expand Down
26 changes: 16 additions & 10 deletions src/app-components/Dropzone/Dropzone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,21 @@ import type { FileRejection } from 'react-dropzone';
import cn from 'classnames';

import classes from 'src/app-components/Dropzone/Dropzone.module.css';
import { mapExtensionToAcceptMime } from 'src/app-components/Dropzone/mapExtensionToAcceptMime';

type MaxFileSize = {
sizeInMB: number;
text: string;
};

export type IDropzoneComponentProps = {
export type IDropzoneProps = {
id: string;
maxFileSize?: MaxFileSize;
readOnly: boolean;
onClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
onClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
onDrop: (acceptedFiles: File[], rejectedFiles: FileRejection[]) => void;
onDragActiveChange?: (isDragActive: boolean) => void;
hasValidationMessages: boolean;
hasCustomFileEndings?: boolean;
validFileEndings?: string | string[];
acceptedFiles?: { [key: string]: string[] };
labelId?: string;
describedBy?: string;
className?: string;
Expand All @@ -35,15 +34,15 @@ export function Dropzone({
readOnly,
onClick,
onDrop,
onDragActiveChange,
hasValidationMessages,
hasCustomFileEndings,
validFileEndings,
acceptedFiles,
labelId,
children,
className,
describedBy,
...rest
}: IDropzoneComponentProps): React.JSX.Element {
}: IDropzoneProps): React.JSX.Element {
const maxSizeLabelId = `file-upload-max-size-${id}`;
const describedby =
[describedBy, maxFileSize?.sizeInMB ? maxSizeLabelId : undefined].filter(Boolean).join(' ') || undefined;
Expand All @@ -52,9 +51,16 @@ export function Dropzone({
onDrop,
maxSize: maxFileSize && maxFileSize.sizeInMB * bytesInOneMB,
disabled: readOnly,
accept:
hasCustomFileEndings && validFileEndings !== undefined ? mapExtensionToAcceptMime(validFileEndings) : undefined,
accept: acceptedFiles,
});

// set drag active state in parent component if callback is provided
React.useEffect(() => {
if (onDragActiveChange) {
onDragActiveChange(isDragActive);
}
}, [isDragActive, onDragActiveChange]);

return (
<div>
{maxFileSize && (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "Display value of FileUpload component",
"expression": [
"displayValue",
"image"
],
"context": {
"component": "image",
"currentLayout": "Page"
},
"expects": "my-image.jpg",
"layouts": {
"Page": {
"$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json",
"data": {
"layout": [
{
"id": "image",
"type": "ImageUpload"
}
]
}
}
},
"instanceDataElements": [
{
"id": "bb2b2222-2b22-2b22-222b-222222222222",
"instanceGuid": "aa1a1111-1a11-1a11-111a-111111111111",
"dataType": "image",
"filename": "my-image.jpg",
"contentType": "image/jpeg",
"blobStoragePath": "",
"size": 100,
"locked": false,
"refs": [],
"created": "2021-01-01T00:00:00.000Z",
"createdBy": "testUser",
"lastChanged": "2021-01-01T00:00:00.000Z",
"lastChangedBy": "testUser"
}
]
}
11 changes: 11 additions & 0 deletions src/language/texts/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,17 @@ export function en() {
'iframe_component.unsupported_browser_title': 'Your browser is unsupported',
'iframe_component.unsupported_browser':
'Your browser does not support iframes that use srcdoc. This may result in not being able to see all the content intended to be displayed here. We recommend trying a different browser.',
'image_upload_component.animated_warning': 'If the image is animated, only the first frame will be shown.',
'image_upload_component.button_change': 'Change image',
'image_upload_component.button_delete': 'Delete image',
'image_upload_component.button_save': 'Save image',
'image_upload_component.crop_area': 'Crop area',
'image_upload_component.slider_zoom': 'Zoom',
'image_upload_component.summary_empty': "You haven't uploaded an image",
'image_upload_component.reset': 'Reset position and zoom',
'image_upload_component.error_invalid_file_type': 'Invalid file format. Please upload an image file.',
'image_upload_component.error_file_size_exceeded': 'File size exceeds 10MB limit.',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked, and I have some JPEGs straight out of my camera that are larger than this. Is there any reason for this limit? Won't this image become way smaller after cropping and resolution-limiting anyway?

Copy link
Contributor

@lassopicasso lassopicasso Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct, the cropped image that is saved becomes way smaller. But it seems like the browser doesn't handle it well when reading larger files, such as 50mbs. This happens when javascript tries to decode the full image into memory and allocate a texture in gpu memory when drawing the image in canvas that is used to display the cropping area. And during this both versions is stored temporarily.

const handleFileUpload = (file: File) => {
    const validationErrors = validateFile(file);
    setValidationErrors(validationErrors);
    if (validationErrors.length > 0) {
      return;
    }

    const reader = new FileReader();
    reader.onload = (event) => {
      const result = event.target?.result;

      if (typeof result === 'string') {
        const img = new Image();
        img.id = file.name;
        imageTypeRef.current = file.type;
        img.onload = () => {
          updateImageState({ minZoom: calculateMinZoom({ img, cropArea }), img });
        };
        img.src = result;
      }
    };
    reader.readAsDataURL(file);
  };

After the load. the browser keeps struggling. So we ended up setting a limit on 10mb to ensure a good user experience on different browsers. With this context in mind, profile images, we thought that most images would probably would be below 10mb. But I tested a bit more, and it seem it maybe can handle up to 20mbs as well with a bit delay at the beginning.

What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see! I thought it might just be an arbitrary limit. It really comes down to your device then. Will it destroy performance even after cropping and uploading (or clearing the image), or is it permanent? If it's slow forever after I'm in favor of a limit, but otherwise maybe an alert is fine (something like the alert for animations)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like it works fine after I manage to save it, but before that everything lags enormously 😄 Panning the cropping area or using zoom can be extremely slow, and one time I got a browser error. I tested with 50mb from https://examplefile.com/image/jpg.
I also tried to upload this kind of image on linkedin, which uses canvas too, and I experienced the same problem there. On linkedin the image hasn't been displayed yet after more than 5 minutes. So our solution handles it better 🥳

As you mention, it could be an good option to display an alert message similar to animations if it exceeds 10mbs (something like: "Bildet er større enn 10 MB. Du kan oppleve at redigering/nettleseren går saktere enn vanlig.").

I’m still a bit in favor of limiting the size, since the UX overall is pretty bad until the user manages to save, but I can sleep on it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries! I'll defer to your judgement here, sounds like you've been digging far deeper into this than me. 🙌

'image_upload_component.valid_file_types': 'Image files only',
'input_components.remaining_characters': 'You have %d characters left',
'input_components.exceeded_max_limit': 'You have exceeded the maximum limit with %d characters',
'instance_selection.changed_by': 'Changed by',
Expand Down
13 changes: 12 additions & 1 deletion src/language/texts/nb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function nb() {
'form_filler.file_upload_valid_file_format_all': 'alle',
'form_filler.file_uploader_add_attachment': 'Legg til flere vedlegg',
'form_filler.file_uploader_drag': 'Dra og slipp eller',
'form_filler.file_uploader_find': 'let etter fil',
'form_filler.file_uploader_find': 'finn fil',
'form_filler.file_uploader_list_delete': 'Slett vedlegg',
'form_filler.file_uploader_delete_warning': 'Er du sikker på at du vil slette dette vedlegget?',
'form_filler.file_uploader_delete_button_confirm': 'Ja, slett vedlegg',
Expand Down Expand Up @@ -209,6 +209,17 @@ export function nb() {
'iframe_component.unsupported_browser_title': 'Nettleseren din støttes ikke',
'iframe_component.unsupported_browser':
'Nettleseren du bruker støtter ikke iframes som benytter seg av srcdoc. Dette kan føre til at du ikke ser all innholdet som er ment å vises her. Vi anbefaler deg å prøve en annen nettleser.',
'image_upload_component.animated_warning': 'Hvis bildet er animert, vises bare det første bildet.',
'image_upload_component.button_change': 'Bytt bilde',
'image_upload_component.button_delete': 'Slett bildet',
'image_upload_component.button_save': 'Lagre bilde',
'image_upload_component.crop_area': 'Beskjæringsområde',
'image_upload_component.slider_zoom': 'Tilpass bildet',
'image_upload_component.summary_empty': 'Du har ikke lastet opp noe bilde',
'image_upload_component.reset': 'Tilbakestill zoom og plassering',
'image_upload_component.error_invalid_file_type': 'Ugyldig filformat. Last opp en bildefil.',
'image_upload_component.error_file_size_exceeded': 'Filen er for stor. Største tillatte filstørrelse er 10MB.',
'image_upload_component.valid_file_types': 'Bildefiler er tillatt',
'input_components.remaining_characters': 'Du har %d tegn igjen',
'input_components.exceeded_max_limit': 'Du har overskredet maks antall tegn med %d',
'instance_selection.changed_by': 'Endret av',
Expand Down
13 changes: 12 additions & 1 deletion src/language/texts/nn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function nn() {
'form_filler.file_upload_valid_file_format_all': 'alle',
'form_filler.file_uploader_add_attachment': 'Legg til fleire vedlegg',
'form_filler.file_uploader_drag': 'Dra og slepp eller',
'form_filler.file_uploader_find': 'leit etter fil',
'form_filler.file_uploader_find': 'finn fil',
'form_filler.file_uploader_list_delete': 'Slett vedlegg',
'form_filler.file_uploader_delete_warning': 'Er du sikker på at du vil sletta dette vedlegget?',
'form_filler.file_uploader_delete_button_confirm': 'Ja, slett vedlegg',
Expand Down Expand Up @@ -209,6 +209,17 @@ export function nn() {
'iframe_component.unsupported_browser_title': 'Nettlesaren din støttas ikkje',
'iframe_component.unsupported_browser':
'Nettlesaren di støttar ikkje iframes som brukar srcdoc. Dette kan føre til at du ikkje ser all innhaldet som er meint å visast her. Vi anbefalar deg å prøve ein annan nettlesar.',
'image_upload_component.animated_warning': 'Hvis bildet er animert, vises bare det første bildet.',
'image_upload_component.button_change': 'Bytt bilde',
Comment on lines +212 to +213
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Translation inconsistencies: Bokmål used instead of Nynorsk.

Lines 212 and 213 use Norwegian Bokmål rather than Nynorsk:

  • Line 212: "Hvis bildet er animert, vises bare det første bildet."

    • "Hvis" should be "Viss" or "Dersom" in Nynorsk
    • "vises" should be "blir vist" in Nynorsk
  • Line 213: "Bytt bilde"

    • "Bytt" should be "Byt" in Nynorsk

Apply this diff to correct the translations:

-    'image_upload_component.animated_warning': 'Hvis bildet er animert, vises bare det første bildet.',
-    'image_upload_component.button_change': 'Bytt bilde',
+    'image_upload_component.animated_warning': 'Viss bildet er animert, blir berre det første bildet vist.',
+    'image_upload_component.button_change': 'Byt bilde',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'image_upload_component.animated_warning': 'Hvis bildet er animert, vises bare det første bildet.',
'image_upload_component.button_change': 'Bytt bilde',
'image_upload_component.animated_warning': 'Viss bildet er animert, blir berre det første bildet vist.',
'image_upload_component.button_change': 'Byt bilde',
🤖 Prompt for AI Agents
In src/language/texts/nn.ts around lines 212 to 213, the two strings use Bokmål;
replace them with Nynorsk equivalents: change line 212 to use "Viss biletet er
animert, blir berre det første biletet vist." and change line 213 to "Byt
bilete" so both strings use Nynorsk spelling and grammar.

'image_upload_component.button_delete': 'Slett bildet',
'image_upload_component.button_save': 'Lagre bilde',
'image_upload_component.slider_zoom': 'Tilpass bildet',
'image_upload_component.crop_area': 'Beskjæringsområde',
'image_upload_component.summary_empty': 'Du har ikkje lasta opp noko bilde',
'image_upload_component.reset': 'Tilbakestill zoom og plassering',
'image_upload_component.error_invalid_file_type': 'Ugyldig filformat. Last opp ein bildefil.',
'image_upload_component.error_file_size_exceeded': 'Fila er for stor. Største tillatte filstorleik er 10MB.',
'image_upload_component.valid_file_types': 'Bildefiler er tillatne',
'input_components.remaining_characters': 'Du har %d teikn igjen',
'input_components.exceeded_max_limit': 'Du har overskride maks teikn med %d',
'instance_selection.changed_by': 'Endra av',
Expand Down
4 changes: 4 additions & 0 deletions src/layout/Cards/Cards.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ video {
.cardMedia:last-of-type {
margin-top: auto;
}

.mediaCard {
margin-bottom: -7px;
}
2 changes: 2 additions & 0 deletions src/layout/Cards/Cards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AppCard } from 'src/app-components/Card/Card';
import { Flex } from 'src/app-components/Flex/Flex';
import { Lang } from 'src/features/language/Lang';
import { CardProvider } from 'src/layout/Cards/CardContext';
import classes from 'src/layout/Cards/Cards.module.css';
import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper';
import { GenericComponent } from 'src/layout/GenericComponent';
import { useHasCapability } from 'src/utils/layout/canRenderIn';
Expand Down Expand Up @@ -117,6 +118,7 @@ function CardItem({ baseComponentId, parentBaseId, isMedia, minMediaHeight }: Ca
<div
data-componentid={id}
data-componentbaseid={baseComponentId}
className={classes.mediaCard}
>
<GenericComponent
baseComponentId={baseComponentId}
Expand Down
2 changes: 1 addition & 1 deletion src/layout/FileUpload/FileUploadComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ describe('File uploading components', () => {
});

expect(screen.getByRole('presentation', { name: /attachment-title/i }).textContent).toMatch(
'Dra og slipp eller let etter filTillatte filformater er: alle',
'Dra og slipp eller finn filTillatte filformater er: alle',
);
});

Expand Down
7 changes: 4 additions & 3 deletions src/layout/FileUpload/FileUploadComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CloudUpIcon } from '@navikt/aksel-icons';
import cn from 'classnames';

import { Dropzone } from 'src/app-components/Dropzone/Dropzone';
import { mapExtensionToAcceptMime } from 'src/app-components/Dropzone/mapExtensionToAcceptMime';
import { getDescriptionId, getLabelId, Label } from 'src/components/label/Label';
import { useAddRejectedAttachments, useAttachmentsFor, useAttachmentsUploader } from 'src/features/attachments/hooks';
import { Lang } from 'src/features/language/Lang';
Expand Down Expand Up @@ -59,7 +60,8 @@ export function FileUploadComponent({
const validations = useUnifiedValidationsForNode(baseComponentId).filter(
(v) => !('attachmentId' in v) || !v.attachmentId,
);

const filesToAccept =
hasCustomFileEndings && validFileEndings !== undefined ? mapExtensionToAcceptMime(validFileEndings) : undefined;
const { options, isFetching } = useGetOptions(baseComponentId, 'single');
const indexedId = useIndexedId(baseComponentId);

Expand Down Expand Up @@ -139,8 +141,7 @@ export function FileUploadComponent({
onClick={(e) => e.preventDefault()}
onDrop={handleDrop}
hasValidationMessages={hasValidationErrors(validations)}
hasCustomFileEndings={hasCustomFileEndings}
validFileEndings={validFileEndings}
acceptedFiles={filesToAccept}
labelId={textResourceBindings?.title ? getLabelId(id) : undefined}
describedBy={ariaDescribedBy}
>
Expand Down
4 changes: 2 additions & 2 deletions src/layout/FileUpload/FileUploadTable/FileTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ export function FileTable({
isSummary,
isFetching,
}: FileTableProps): React.JSX.Element | null {
const { textResourceBindings, type, readOnly } = useItemWhenType<'FileUpload' | 'FileUploadWithTag'>(
const { textResourceBindings, type, readOnly } = useItemWhenType<'FileUpload' | 'FileUploadWithTag' | 'ImageUpload'>(
baseComponentId,
(t) => t === 'FileUpload' || t === 'FileUploadWithTag',
(t) => t === 'FileUpload' || t === 'FileUploadWithTag' || t === 'ImageUpload',
);
const hasTag = type === 'FileUploadWithTag';
const pdfModeActive = usePdfModeActive();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from 'src/utils/layout/generator/validation/hooks';
import type { IDataModelBindings } from 'src/layout/layout';

export function useFileUploaderDataBindingsValidation<T extends 'FileUpload' | 'FileUploadWithTag'>(
export function useFileUploaderDataBindingsValidation<T extends 'FileUpload' | 'FileUploadWithTag' | 'ImageUpload'>(
baseComponentId: string,
bindings: IDataModelBindings<T>,
): string[] {
Expand Down
12 changes: 12 additions & 0 deletions src/layout/ImageUpload/ImageCanvas/ImageCanvas.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.canvas {
display: block;
position: relative;
left: 50%;
transform: translateX(-50%);
cursor: grab;
touch-action: none;
}

.canvas:active {
cursor: grabbing;
}
65 changes: 65 additions & 0 deletions src/layout/ImageUpload/ImageCanvas/ImageCanvas.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';

import { useLanguage } from 'src/features/language/useLanguage';
import { useCanvasDraw } from 'src/layout/ImageUpload/ImageCanvas/hooks/useCanvasDraw';
import { useDragInteraction } from 'src/layout/ImageUpload/ImageCanvas/hooks/useDragInteraction';
import { useKeyboardNavigation } from 'src/layout/ImageUpload/ImageCanvas/hooks/useKeyboardNavigation';
import { useZoomInteraction } from 'src/layout/ImageUpload/ImageCanvas/hooks/useZoomInteraction';
import classes from 'src/layout/ImageUpload/ImageCanvas/ImageCanvas.module.css';
import { ImagePreview } from 'src/layout/ImageUpload/ImageCanvas/ImagePreview';
import { useImageFile } from 'src/layout/ImageUpload/useImageFile';
import type { CropArea, Position } from 'src/layout/ImageUpload/imageUploadUtils';
interface ImageCanvasProps {
imageRef: React.RefObject<HTMLImageElement | null>;
zoom: number;
position: Position;
cropArea: CropArea;
baseComponentId: string;
onPositionChange: (newPosition: Position) => void;
onZoomChange: (newZoom: number) => void;
canvasRef: React.RefObject<HTMLCanvasElement | null>;
}

const CANVAS_HEIGHT = 320;
const CANVAS_WIDTH = 1000;

export function ImageCanvas({
imageRef,
zoom,
position,
cropArea,
baseComponentId,
onPositionChange,
onZoomChange,
canvasRef,
}: ImageCanvasProps) {
const { storedImage, imageUrl } = useImageFile(baseComponentId);
const { langAsString } = useLanguage();

useCanvasDraw({ canvasRef, imageRef, zoom, position, cropArea });
useZoomInteraction({ canvasRef, zoom, onZoomChange });
const { handlePointerDown } = useDragInteraction({ canvasRef, position, onPositionChange });
const { handleKeyDown } = useKeyboardNavigation({ position, onPositionChange });

if (storedImage) {
return (
<ImagePreview
storedImage={storedImage}
imageUrl={imageUrl}
/>
);
}

return (
<canvas
onPointerDown={handlePointerDown}
onKeyDown={handleKeyDown}
tabIndex={0}
ref={canvasRef}
height={CANVAS_HEIGHT}
width={CANVAS_WIDTH}
className={classes.canvas}
aria-label={langAsString('image_upload_component.crop_area')}
/>
);
}
9 changes: 9 additions & 0 deletions src/layout/ImageUpload/ImageCanvas/ImagePreview.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.previewWrapper {
background-color: #f4f5f6; /* Following does not exist in v1: var(--ds-color-neutral-background-subtle); */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use a different color here? If this color already exists elsewhere, we should align with that and reference a shared CSS variable/design token. If it’s specific to this component only, consider introducing a v1 component-scoped token instead of hard-coding the value.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will look for a similar token instead :)

height: 320px;
display: flex;
justify-content: center;
align-items: center;
padding: var(--ds-size-4);
box-sizing: border-box;
}
Loading
Loading