@helsenorge/lightbox 12.10.0 → 12.11.2-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__scripts__/entries.js +23 -0
- package/package.json +67 -3
- package/src/components/LightBox/LightBox.stories.tsx +123 -0
- package/src/components/LightBox/LightBox.test.tsx +98 -0
- package/src/components/LightBox/LightBox.tsx +285 -0
- package/src/components/LightBox/MiniSlider.tsx +44 -0
- package/{components/LightBox/index.d.ts → src/components/LightBox/index.ts} +1 -1
- package/tsconfig.json +9 -0
- package/tsconfig.node.json +10 -0
- package/vite.config.ts +65 -0
- package/vitest.config.ts +28 -0
- package/CHANGELOG.md +0 -3328
- package/__mocks__/styleMock.js +0 -2
- package/__mocks__/styleMock.js.map +0 -1
- package/components/LightBox/LightBox.d.ts +0 -39
- package/components/LightBox/LightBox.test.d.ts +0 -0
- package/components/LightBox/MiniSlider.d.ts +0 -10
- package/components/LightBox/index.js +0 -1941
- package/components/LightBox/index.js.map +0 -1
- /package/{__mocks__/styleMock.d.ts → src/__mocks__/styleMock.ts} +0 -0
- /package/{components → src/components}/LightBox/styles.module.scss +0 -0
- /package/{components → src/components}/LightBox/styles.module.scss.d.ts +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { globSync } from 'glob';
|
|
2
|
+
|
|
3
|
+
const getEntryName = name => {
|
|
4
|
+
return name.replace(/^src[/\\]/, '').replace(/\.tsx?$/, '');
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const createEntries = (entryList, entry) => {
|
|
8
|
+
const name = getEntryName(entry);
|
|
9
|
+
|
|
10
|
+
if (name in entryList) {
|
|
11
|
+
throw new Error(`${name} finnes flere ganger i listen med entries`);
|
|
12
|
+
}
|
|
13
|
+
entryList[name] = entry;
|
|
14
|
+
|
|
15
|
+
return entryList;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const alwaysIgnore = ['**/__snapshots__/**/*', '**/*.stories.tsx', '**/*.test.tsx', '**/*.scss', '**/*.scss.d.ts', '**/*.d.ts'];
|
|
19
|
+
|
|
20
|
+
const components = globSync(`src/components/**/index.{ts,tsx}`, { ignore: alwaysIgnore });
|
|
21
|
+
const hooksAndExtras = globSync(`src/**/*.{ts,tsx}`, { ignore: [...alwaysIgnore, 'src/components/**/*'] });
|
|
22
|
+
|
|
23
|
+
export const entries = [...components, ...hooksAndExtras].sort((a, b) => a.localeCompare(b)).reduce(createEntries, {});
|
package/package.json
CHANGED
|
@@ -1,20 +1,84 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helsenorge/lightbox",
|
|
3
|
-
"
|
|
3
|
+
"sideEffects": false,
|
|
4
|
+
"private": false,
|
|
5
|
+
"version": "12.11.2-beta.0",
|
|
4
6
|
"description": "The official Helsenorge lightbox.",
|
|
5
7
|
"repository": {
|
|
6
8
|
"type": "git",
|
|
7
9
|
"url": "git+https://github.com/helsenorge/designsystem.git"
|
|
8
10
|
},
|
|
9
11
|
"homepage": "https://helsenorge.design",
|
|
10
|
-
"version": "12.10.0",
|
|
11
12
|
"author": "Helsenorge",
|
|
13
|
+
"maintainers": [
|
|
14
|
+
"Frankenstein"
|
|
15
|
+
],
|
|
12
16
|
"license": "MIT",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"directory": "lib",
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"main": "./lib/index.js",
|
|
23
|
+
"module": "./lib/index.js",
|
|
24
|
+
"types": "./lib/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./lib/index.d.ts",
|
|
28
|
+
"import": "./lib/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./components/*": {
|
|
31
|
+
"types": "./lib/components/*/index.d.ts",
|
|
32
|
+
"import": "./lib/components/*/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./scss/*": "./lib/scss/*",
|
|
35
|
+
"./package.json": "./package.json"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"prebuild": "npm-run-all clean generate:cssdefinitions",
|
|
39
|
+
"build": "vite build",
|
|
40
|
+
"clean": "rimraf lib",
|
|
41
|
+
"generate:cssdefinitions": "typed-scss-modules \"src/**/*.module.scss\" --nameFormat none --exportType default --includePaths node_modules ../../node_modules",
|
|
42
|
+
"eslint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
43
|
+
"eslint:fix": "npm run eslint -- --fix",
|
|
44
|
+
"stylelint": "stylelint \"src/**/*.{css,scss}\"",
|
|
45
|
+
"stylelint:fix": "npm run stylelint -- --fix",
|
|
46
|
+
"prettier": "prettier --check \"src/**/*.{js,jsx,ts,tsx,css,scss,md,json}\"",
|
|
47
|
+
"prettier:fix": "npm run prettier -- --write",
|
|
48
|
+
"pretypecheck": "tsc -v",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"test": "vitest"
|
|
51
|
+
},
|
|
13
52
|
"peerDependencies": {
|
|
14
53
|
"@helsenorge/designsystem-react": "^12.0.0",
|
|
15
54
|
"classnames": "^2.5.1",
|
|
16
55
|
"react": "^18.0.0",
|
|
17
56
|
"react-dom": "^18.0.0"
|
|
18
57
|
},
|
|
19
|
-
"
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@helsenorge/designsystem-react": "^12.11.1",
|
|
60
|
+
"@helsenorge/typed-scss-modules": "^9.0.0",
|
|
61
|
+
"@rollup/plugin-replace": "^6.0.1",
|
|
62
|
+
"@testing-library/jest-dom": "^6.5.0",
|
|
63
|
+
"@testing-library/react": "^16.0.1",
|
|
64
|
+
"@testing-library/user-event": "^14.6.1",
|
|
65
|
+
"@types/react": "^18.3.11",
|
|
66
|
+
"@types/react-dom": "^18.3.1",
|
|
67
|
+
"@vitejs/plugin-react": "^4.3.2",
|
|
68
|
+
"@vitest/coverage-istanbul": "^3.1.2",
|
|
69
|
+
"@vitest/coverage-v8": "^3.1.2",
|
|
70
|
+
"classnames": "^2.5.1",
|
|
71
|
+
"frankenstein-build-tools": "file:../build-tools",
|
|
72
|
+
"jsdom": "^26.0.0",
|
|
73
|
+
"react": "^18.3.1",
|
|
74
|
+
"react-dom": "^18.3.1",
|
|
75
|
+
"react-zoom-pan-pinch": "^3.6.1",
|
|
76
|
+
"rollup-plugin-copy": "^3.5.0",
|
|
77
|
+
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
78
|
+
"rollup-plugin-visualizer": "^5.12.0",
|
|
79
|
+
"typescript": "~5.8.2",
|
|
80
|
+
"vite": "^6.2.1",
|
|
81
|
+
"vite-plugin-dts": "^4.3.0",
|
|
82
|
+
"vitest": "^3.1.2"
|
|
83
|
+
}
|
|
20
84
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { StoryObj, Meta } from '@storybook/react-vite';
|
|
4
|
+
import { Docs } from 'frankenstein-build-tools';
|
|
5
|
+
import { action } from 'storybook/actions';
|
|
6
|
+
|
|
7
|
+
import longLoremText from '@helsenorge/designsystem-react/utils/loremtext';
|
|
8
|
+
|
|
9
|
+
import LightBox from './LightBox';
|
|
10
|
+
|
|
11
|
+
const meta = {
|
|
12
|
+
title: '@helsenorge/lightbox/LightBox',
|
|
13
|
+
component: LightBox,
|
|
14
|
+
parameters: {
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component: 'Beskrivelse av LightBox',
|
|
18
|
+
},
|
|
19
|
+
story: {
|
|
20
|
+
inline: false,
|
|
21
|
+
iframeHeight: '40rem',
|
|
22
|
+
},
|
|
23
|
+
page: (): React.JSX.Element => <Docs component={LightBox} />,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
args: {
|
|
27
|
+
ariaLabelCloseButton: 'Lukk Lightbox',
|
|
28
|
+
ariaLabelLeftArrow: 'Forrige bilde',
|
|
29
|
+
ariaLabelLightBox: 'Bildevisning',
|
|
30
|
+
ariaLabelRightArrow: 'Neste bilde',
|
|
31
|
+
ariaLabelCloseTextBox: 'Lukk tekstboks',
|
|
32
|
+
ariaLabelOpenTextBox: 'Åpne tekstboks',
|
|
33
|
+
ariaLabelZoomIn: 'Zoom inn',
|
|
34
|
+
ariaLabelZoomOut: 'Zoom ut',
|
|
35
|
+
ariaLabelZoomSlider: 'Zoom',
|
|
36
|
+
closeTextAfterSeconds: 3,
|
|
37
|
+
imageAlt: 'A random cat',
|
|
38
|
+
imageSrc: 'https://placehold.co/640x480',
|
|
39
|
+
testId: 'lightBox',
|
|
40
|
+
imageText: 'En søt pus eller kanskje flere',
|
|
41
|
+
onClose: action('Close button clicked'),
|
|
42
|
+
onLeftArrowClick: action('Left arrow clicked'),
|
|
43
|
+
onRightArrowClick: action('Right arrow clicked'),
|
|
44
|
+
},
|
|
45
|
+
argTypes: {},
|
|
46
|
+
} satisfies Meta<typeof LightBox>;
|
|
47
|
+
|
|
48
|
+
export default meta;
|
|
49
|
+
|
|
50
|
+
type Story = StoryObj<typeof meta>;
|
|
51
|
+
|
|
52
|
+
export const Default: Story = {
|
|
53
|
+
render: args => <LightBox {...args} />,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const LangBildetekst: Story = {
|
|
57
|
+
render: args => (
|
|
58
|
+
<LightBox
|
|
59
|
+
{...args}
|
|
60
|
+
imageText={
|
|
61
|
+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eu turpis posuere, dignissim ex at, interdum metus. Etiam efficitur ut lectus et condimentum. Suspendisse ornare suscipit metus sit amet luctus. Quisque risus orci, molestie sit amet tempus non, semper a ligula. Etiam volutpat scelerisque magna vel feugiat. Sed ac venenatis justo. Aliquam iaculis ante a eros sagittis, id gravida felis placerat. Cras luctus mi quam, non venenatis lorem condimentum vel. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Integer nulla sem, placerat non blandit ac, interdum vel augue. Nulla fermentum orci non augue pulvinar, sed posuere neque scelerisque. Aenean pulvinar commodo lorem vel consectetur. Nam augue lectus, tempus vitae finibus id, dignissim id eros. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean pulvinar commodo lorem vel consectetur. Nam augue lectus, tempus vitae finibus id, dignissim id eros. Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
|
62
|
+
}
|
|
63
|
+
closeTextAfterSeconds={undefined}
|
|
64
|
+
/>
|
|
65
|
+
),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const AapnesOverSideRender = (args: React.ComponentProps<typeof LightBox>): React.ReactElement => {
|
|
69
|
+
const [lightboxOpen, setLightboxOpen] = React.useState(false);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div>
|
|
73
|
+
<button onClick={() => setLightboxOpen(true)} id="åpne">
|
|
74
|
+
{'Åpne LightBox over side'}
|
|
75
|
+
</button>
|
|
76
|
+
|
|
77
|
+
<section>{longLoremText}</section>
|
|
78
|
+
<section>{longLoremText}</section>
|
|
79
|
+
<section>{longLoremText}</section>
|
|
80
|
+
|
|
81
|
+
<button id="tilfeldig" onClick={() => null}>
|
|
82
|
+
{'Tilfeldig knapp'}
|
|
83
|
+
</button>
|
|
84
|
+
|
|
85
|
+
<section>{longLoremText}</section>
|
|
86
|
+
<section>{longLoremText}</section>
|
|
87
|
+
<section>{longLoremText}</section>
|
|
88
|
+
|
|
89
|
+
{lightboxOpen && (
|
|
90
|
+
<LightBox
|
|
91
|
+
{...args}
|
|
92
|
+
onClose={() => setLightboxOpen(false)}
|
|
93
|
+
imageText="Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
|
|
94
|
+
closeTextAfterSeconds={undefined}
|
|
95
|
+
/>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const ÅpnesOverSide: Story = {
|
|
102
|
+
render: args => <AapnesOverSideRender {...args} />,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const EgetOppsettPåTekst: Story = {
|
|
106
|
+
args: {
|
|
107
|
+
closeTextAfterSeconds: undefined,
|
|
108
|
+
},
|
|
109
|
+
render: args => (
|
|
110
|
+
<LightBox
|
|
111
|
+
{...args}
|
|
112
|
+
imageText={
|
|
113
|
+
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
114
|
+
<span>{'Tekst over flere'}</span>
|
|
115
|
+
<span>
|
|
116
|
+
{'linjer og med '}
|
|
117
|
+
<strong>{'styling'}</strong>
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
}
|
|
121
|
+
/>
|
|
122
|
+
),
|
|
123
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import '@testing-library/jest-dom';
|
|
3
|
+
|
|
4
|
+
import LightBox from './LightBox';
|
|
5
|
+
|
|
6
|
+
describe('Gitt at LightBox skal vises', (): void => {
|
|
7
|
+
describe('Når LightBox vises', (): void => {
|
|
8
|
+
test('Så vises LightBox', (): void => {
|
|
9
|
+
render(
|
|
10
|
+
<LightBox
|
|
11
|
+
ariaLabelCloseButton={''}
|
|
12
|
+
ariaLabelLeftArrow={''}
|
|
13
|
+
ariaLabelRightArrow={''}
|
|
14
|
+
ariaLabelCloseTextBox={''}
|
|
15
|
+
ariaLabelOpenTextBox={''}
|
|
16
|
+
imageAlt={''}
|
|
17
|
+
imageSrc={''}
|
|
18
|
+
onClose={() => null}
|
|
19
|
+
ariaLabelZoomOut={''}
|
|
20
|
+
ariaLabelZoomIn={''}
|
|
21
|
+
ariaLabelLightBox={''}
|
|
22
|
+
ariaLabelZoomSlider={''}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('Så vises lukkeknapp med riktig label', (): void => {
|
|
28
|
+
render(
|
|
29
|
+
<LightBox
|
|
30
|
+
ariaLabelCloseButton="Lukk"
|
|
31
|
+
ariaLabelLeftArrow=""
|
|
32
|
+
ariaLabelRightArrow=""
|
|
33
|
+
ariaLabelCloseTextBox=""
|
|
34
|
+
ariaLabelOpenTextBox=""
|
|
35
|
+
imageAlt=""
|
|
36
|
+
imageSrc=""
|
|
37
|
+
onClose={() => null}
|
|
38
|
+
ariaLabelZoomOut=""
|
|
39
|
+
ariaLabelZoomIn=""
|
|
40
|
+
ariaLabelLightBox={''}
|
|
41
|
+
ariaLabelZoomSlider={''}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const closeButton = screen.getByLabelText('Lukk');
|
|
46
|
+
expect(closeButton).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('Så vises begge pilknapper når callback er gitt', (): void => {
|
|
50
|
+
render(
|
|
51
|
+
<LightBox
|
|
52
|
+
ariaLabelCloseButton=""
|
|
53
|
+
ariaLabelLeftArrow="Forrige bilde"
|
|
54
|
+
onLeftArrowClick={() => null}
|
|
55
|
+
ariaLabelRightArrow="Neste bilde"
|
|
56
|
+
onRightArrowClick={() => null}
|
|
57
|
+
ariaLabelCloseTextBox=""
|
|
58
|
+
ariaLabelOpenTextBox=""
|
|
59
|
+
imageAlt=""
|
|
60
|
+
imageSrc=""
|
|
61
|
+
onClose={() => null}
|
|
62
|
+
ariaLabelZoomOut=""
|
|
63
|
+
ariaLabelZoomIn=""
|
|
64
|
+
ariaLabelLightBox={''}
|
|
65
|
+
ariaLabelZoomSlider={''}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const rightArrow = screen.getByLabelText('Neste bilde');
|
|
70
|
+
const leftArrow = screen.getByLabelText('Forrige bilde');
|
|
71
|
+
expect(rightArrow).toBeInTheDocument();
|
|
72
|
+
expect(leftArrow).toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('Så blir onClose funksjonen kalt når lukkeknappen trykkes', (): void => {
|
|
76
|
+
const onCloseMock = vi.fn();
|
|
77
|
+
render(
|
|
78
|
+
<LightBox
|
|
79
|
+
ariaLabelCloseButton="Lukk"
|
|
80
|
+
ariaLabelLeftArrow=""
|
|
81
|
+
ariaLabelRightArrow=""
|
|
82
|
+
ariaLabelCloseTextBox=""
|
|
83
|
+
ariaLabelOpenTextBox=""
|
|
84
|
+
imageAlt=""
|
|
85
|
+
imageSrc=""
|
|
86
|
+
onClose={onCloseMock}
|
|
87
|
+
ariaLabelZoomOut=""
|
|
88
|
+
ariaLabelZoomIn=""
|
|
89
|
+
ariaLabelLightBox={''}
|
|
90
|
+
ariaLabelZoomSlider={''}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
const closeButton = screen.getByLabelText('Lukk');
|
|
94
|
+
fireEvent.click(closeButton);
|
|
95
|
+
expect(onCloseMock).toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import { TransformWrapper, TransformComponent, useTransformComponent, useControls } from 'react-zoom-pan-pinch';
|
|
5
|
+
|
|
6
|
+
import Icon from '@helsenorge/designsystem-react/components/Icon';
|
|
7
|
+
import ChevronLeft from '@helsenorge/designsystem-react/components/Icons/ChevronLeft';
|
|
8
|
+
import ChevronRight from '@helsenorge/designsystem-react/components/Icons/ChevronRight';
|
|
9
|
+
import ChevronsDown from '@helsenorge/designsystem-react/components/Icons/ChevronsDown';
|
|
10
|
+
import ChevronsUp from '@helsenorge/designsystem-react/components/Icons/ChevronsUp';
|
|
11
|
+
import Minus from '@helsenorge/designsystem-react/components/Icons/Minus';
|
|
12
|
+
import PlusSmall from '@helsenorge/designsystem-react/components/Icons/PlusSmall';
|
|
13
|
+
import X from '@helsenorge/designsystem-react/components/Icons/X';
|
|
14
|
+
import { IconSize, KeyboardEventKey, ZIndex } from '@helsenorge/designsystem-react/constants';
|
|
15
|
+
import { useFocusTrap } from '@helsenorge/designsystem-react/hooks/useFocusTrap';
|
|
16
|
+
import { useReturnFocusOnUnmount } from '@helsenorge/designsystem-react/hooks/useReturnFocusOnUnmount';
|
|
17
|
+
import { useSize } from '@helsenorge/designsystem-react/hooks/useSize';
|
|
18
|
+
import { disableBodyScroll, enableBodyScroll } from '@helsenorge/designsystem-react/utils/scroll';
|
|
19
|
+
|
|
20
|
+
import { useKeyboardEvent } from '@helsenorge/designsystem-react';
|
|
21
|
+
|
|
22
|
+
import MiniSlider from './MiniSlider';
|
|
23
|
+
|
|
24
|
+
import styles from './styles.module.scss';
|
|
25
|
+
|
|
26
|
+
export interface LightBoxProps {
|
|
27
|
+
/** Aria label for the close button */
|
|
28
|
+
ariaLabelCloseButton: string;
|
|
29
|
+
/** Aria label for the text box button when its open */
|
|
30
|
+
ariaLabelCloseTextBox: string;
|
|
31
|
+
/** Aria label for the left arrow button */
|
|
32
|
+
ariaLabelLeftArrow?: string;
|
|
33
|
+
/** Aria label for the full modal describing what the modal contains */
|
|
34
|
+
ariaLabelLightBox: string;
|
|
35
|
+
/** Aria label for the right arrow button */
|
|
36
|
+
ariaLabelRightArrow?: string;
|
|
37
|
+
/** Aria label for the text box button when its closed */
|
|
38
|
+
ariaLabelOpenTextBox: string;
|
|
39
|
+
/** Aria label for the zoom in button */
|
|
40
|
+
ariaLabelZoomIn: string;
|
|
41
|
+
/** Aria label for the zoom out button */
|
|
42
|
+
ariaLabelZoomOut: string;
|
|
43
|
+
/** Aria label for the slider input component */
|
|
44
|
+
ariaLabelZoomSlider: string;
|
|
45
|
+
/** If set the text box closes automatically after the given seconds */
|
|
46
|
+
closeTextAfterSeconds?: number;
|
|
47
|
+
/** Alt text for the image */
|
|
48
|
+
imageAlt: string;
|
|
49
|
+
/** Source of the image that will be shown */
|
|
50
|
+
imageSrc: string;
|
|
51
|
+
/** The text for the image that shows in the textbox */
|
|
52
|
+
imageText?: string | React.ReactNode;
|
|
53
|
+
/** Function is called when user clicks the close button */
|
|
54
|
+
onClose: () => void;
|
|
55
|
+
/** Function is called when user clicks the left arrow button. If not given the arrow will not show. */
|
|
56
|
+
onLeftArrowClick?: () => void;
|
|
57
|
+
/** Function is called when user clicks the right arrow button. If not given the arrow will not show. */
|
|
58
|
+
onRightArrowClick?: () => void;
|
|
59
|
+
/** Sets the data-testid attribute. */
|
|
60
|
+
testId?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const LightBox: React.FC<LightBoxProps> = ({
|
|
64
|
+
ariaLabelCloseButton,
|
|
65
|
+
ariaLabelLeftArrow,
|
|
66
|
+
ariaLabelLightBox,
|
|
67
|
+
ariaLabelRightArrow,
|
|
68
|
+
ariaLabelCloseTextBox,
|
|
69
|
+
ariaLabelOpenTextBox,
|
|
70
|
+
ariaLabelZoomIn,
|
|
71
|
+
ariaLabelZoomOut,
|
|
72
|
+
ariaLabelZoomSlider,
|
|
73
|
+
closeTextAfterSeconds,
|
|
74
|
+
imageAlt,
|
|
75
|
+
imageSrc,
|
|
76
|
+
imageText,
|
|
77
|
+
onClose,
|
|
78
|
+
onLeftArrowClick,
|
|
79
|
+
onRightArrowClick,
|
|
80
|
+
testId,
|
|
81
|
+
}) => {
|
|
82
|
+
const [imageTextOpen, setImageTextOpen] = React.useState(true);
|
|
83
|
+
const lightBoxRef = useRef<HTMLDivElement>(null);
|
|
84
|
+
const textBoxRef = useRef<HTMLParagraphElement>(null);
|
|
85
|
+
const { height: textBoxHeight = 0 } = useSize(textBoxRef) || {};
|
|
86
|
+
const [zoom, setZoom] = useState(1.0);
|
|
87
|
+
useFocusTrap(lightBoxRef, true);
|
|
88
|
+
useReturnFocusOnUnmount(lightBoxRef);
|
|
89
|
+
|
|
90
|
+
useKeyboardEvent(lightBoxRef, onClose, [KeyboardEventKey.Escape]);
|
|
91
|
+
|
|
92
|
+
const updateStates = (newZoom: number): void => {
|
|
93
|
+
if (zoom === newZoom) return;
|
|
94
|
+
setZoom(newZoom);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (!closeTextAfterSeconds) return;
|
|
99
|
+
const timer = setTimeout(() => {
|
|
100
|
+
setImageTextOpen(false);
|
|
101
|
+
}, closeTextAfterSeconds * 1000);
|
|
102
|
+
return (): void => {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
};
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
lightBoxRef.current?.focus();
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div
|
|
113
|
+
data-testid={testId}
|
|
114
|
+
className={styles.lightBox}
|
|
115
|
+
style={{ zIndex: ZIndex.OverlayScreen }}
|
|
116
|
+
role="dialog"
|
|
117
|
+
aria-modal={true}
|
|
118
|
+
tabIndex={-1}
|
|
119
|
+
aria-label={ariaLabelLightBox}
|
|
120
|
+
ref={lightBoxRef}
|
|
121
|
+
>
|
|
122
|
+
<button
|
|
123
|
+
onClick={onClose}
|
|
124
|
+
aria-label={ariaLabelCloseButton}
|
|
125
|
+
data-testid="closeButton"
|
|
126
|
+
className={classNames(styles.button, styles['close-button'])}
|
|
127
|
+
style={{ zIndex: ZIndex.LightBoxButtons }}
|
|
128
|
+
>
|
|
129
|
+
<Icon svgIcon={X} color="white" size={IconSize.XSmall} />
|
|
130
|
+
</button>
|
|
131
|
+
{onLeftArrowClick && (
|
|
132
|
+
<button
|
|
133
|
+
className={classNames(styles.button, styles['arrow-button'], styles['arrow-button--left'])}
|
|
134
|
+
onClick={onLeftArrowClick}
|
|
135
|
+
aria-label={ariaLabelLeftArrow}
|
|
136
|
+
data-testid="leftArrow"
|
|
137
|
+
style={{ zIndex: ZIndex.LightBoxButtons }}
|
|
138
|
+
>
|
|
139
|
+
<Icon svgIcon={ChevronLeft} color="white" size={IconSize.XSmall} />
|
|
140
|
+
</button>
|
|
141
|
+
)}
|
|
142
|
+
{onRightArrowClick && (
|
|
143
|
+
<button
|
|
144
|
+
className={classNames(styles.button, styles['arrow-button'], styles['arrow-button--right'])}
|
|
145
|
+
onClick={onRightArrowClick}
|
|
146
|
+
aria-label={ariaLabelRightArrow}
|
|
147
|
+
data-testid="rightarrow"
|
|
148
|
+
style={{ zIndex: ZIndex.LightBoxButtons }}
|
|
149
|
+
>
|
|
150
|
+
<Icon svgIcon={ChevronRight} color="white" size={IconSize.XSmall} />
|
|
151
|
+
</button>
|
|
152
|
+
)}
|
|
153
|
+
{imageText && (
|
|
154
|
+
<div
|
|
155
|
+
className={styles['image-text-box']}
|
|
156
|
+
style={{ bottom: imageTextOpen ? '0' : '-' + textBoxHeight + 'px', transition: '0.5s', zIndex: ZIndex.LightBoxButtons }}
|
|
157
|
+
>
|
|
158
|
+
<button
|
|
159
|
+
className={classNames(styles.button, styles['image-text-box__button'])}
|
|
160
|
+
onClick={() => setImageTextOpen(!imageTextOpen)}
|
|
161
|
+
style={{ zIndex: ZIndex.LightBoxButtons }}
|
|
162
|
+
aria-label={imageTextOpen ? ariaLabelCloseTextBox : ariaLabelOpenTextBox}
|
|
163
|
+
aria-expanded={imageTextOpen}
|
|
164
|
+
>
|
|
165
|
+
{imageTextOpen ? (
|
|
166
|
+
<Icon svgIcon={ChevronsDown} color="white" size={IconSize.XSmall} />
|
|
167
|
+
) : (
|
|
168
|
+
<Icon svgIcon={ChevronsUp} color="white" size={IconSize.XSmall} />
|
|
169
|
+
)}
|
|
170
|
+
</button>
|
|
171
|
+
<div>
|
|
172
|
+
{typeof imageText === 'string' ? (
|
|
173
|
+
<p ref={textBoxRef} className={styles['image-text-box__text']}>
|
|
174
|
+
{imageText}
|
|
175
|
+
</p>
|
|
176
|
+
) : (
|
|
177
|
+
<div ref={textBoxRef} className={styles['image-text-box__text']}>
|
|
178
|
+
{imageText}
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
<div className={styles['image-text-box__overflow-border']}></div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
<TransformWrapper smooth={false} initialScale={1} maxScale={4} doubleClick={{ mode: 'toggle', step: 4 }}>
|
|
186
|
+
{({ setTransform }) => (
|
|
187
|
+
<>
|
|
188
|
+
<Controls
|
|
189
|
+
transform={setTransform}
|
|
190
|
+
updateStates={updateStates}
|
|
191
|
+
zoom={zoom}
|
|
192
|
+
ariaLabelZoomIn={ariaLabelZoomIn}
|
|
193
|
+
ariaLabelZoomOut={ariaLabelZoomOut}
|
|
194
|
+
ariaLabelZoomSlider={ariaLabelZoomSlider}
|
|
195
|
+
/>
|
|
196
|
+
<TransformComponent
|
|
197
|
+
wrapperStyle={{
|
|
198
|
+
zIndex: 1,
|
|
199
|
+
width: '100%',
|
|
200
|
+
height: '100%',
|
|
201
|
+
}}
|
|
202
|
+
contentStyle={{
|
|
203
|
+
width: '100%',
|
|
204
|
+
height: '100%',
|
|
205
|
+
}}
|
|
206
|
+
>
|
|
207
|
+
<img src={imageSrc} alt={imageAlt} />
|
|
208
|
+
</TransformComponent>
|
|
209
|
+
</>
|
|
210
|
+
)}
|
|
211
|
+
</TransformWrapper>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const Controls = ({
|
|
217
|
+
transform,
|
|
218
|
+
updateStates,
|
|
219
|
+
zoom,
|
|
220
|
+
ariaLabelZoomIn,
|
|
221
|
+
ariaLabelZoomOut,
|
|
222
|
+
ariaLabelZoomSlider,
|
|
223
|
+
}: {
|
|
224
|
+
transform: (newPositionX: number, newPositionY: number, newScale: number, animationTime?: number | undefined) => void;
|
|
225
|
+
updateStates: (newZoom: number) => void;
|
|
226
|
+
zoom: number;
|
|
227
|
+
ariaLabelZoomIn: string;
|
|
228
|
+
ariaLabelZoomOut: string;
|
|
229
|
+
ariaLabelZoomSlider: string;
|
|
230
|
+
}): React.JSX.Element => {
|
|
231
|
+
useTransformComponent(({ state }) => {
|
|
232
|
+
updateStates(state.scale);
|
|
233
|
+
});
|
|
234
|
+
const { zoomIn, zoomOut, centerView } = useControls();
|
|
235
|
+
let centerTimeout: number;
|
|
236
|
+
|
|
237
|
+
const calculateZoomCenter = (newScale: number): number[] => {
|
|
238
|
+
const element = document.getElementsByClassName('react-transform-component')[0];
|
|
239
|
+
const style = window.getComputedStyle(element);
|
|
240
|
+
const matrix = new WebKitCSSMatrix(style.transform);
|
|
241
|
+
const ratio = (newScale - zoom) / zoom + 1;
|
|
242
|
+
const x = (matrix.m41 - (window.innerWidth / 2) * (1 - zoom / newScale)) * ratio;
|
|
243
|
+
const y = (matrix.m42 - (window.innerHeight / 2) * (1 - zoom / newScale)) * ratio;
|
|
244
|
+
return [x, y];
|
|
245
|
+
};
|
|
246
|
+
const adjustZoom = (newScale: number | undefined): void => {
|
|
247
|
+
if (newScale === undefined || newScale === zoom) return;
|
|
248
|
+
if (newScale < 1) newScale = 1;
|
|
249
|
+
if (newScale > 4) newScale = 4;
|
|
250
|
+
const [x, y] = calculateZoomCenter(newScale);
|
|
251
|
+
transform(x, y, newScale, 1);
|
|
252
|
+
|
|
253
|
+
if (centerTimeout) {
|
|
254
|
+
clearTimeout(centerTimeout);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Starter sentrering timeout hvis det zoomes ut
|
|
258
|
+
if (newScale - zoom < 0) {
|
|
259
|
+
centerTimeout = window.setTimeout(() => {
|
|
260
|
+
centerView();
|
|
261
|
+
}, 160);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
disableBodyScroll();
|
|
267
|
+
return (): void => {
|
|
268
|
+
enableBodyScroll();
|
|
269
|
+
};
|
|
270
|
+
}, []);
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<div className={classNames(styles['zoom-buttons'])} style={{ zIndex: ZIndex.LightBoxButtons }}>
|
|
274
|
+
<button className={classNames(styles.button)} onClick={() => zoomOut()} aria-label={ariaLabelZoomOut}>
|
|
275
|
+
<Icon svgIcon={Minus} color="white" size={IconSize.XSmall} />
|
|
276
|
+
</button>
|
|
277
|
+
<MiniSlider minValue={1} maxValue={4} onChange={adjustZoom} value={zoom} ariaLabel={ariaLabelZoomSlider} />
|
|
278
|
+
<button className={classNames(styles.button)} onClick={() => zoomIn()} aria-label={ariaLabelZoomIn}>
|
|
279
|
+
<Icon svgIcon={PlusSmall} color="white" size={IconSize.XSmall} />
|
|
280
|
+
</button>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export default LightBox;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React, { ChangeEvent } from 'react';
|
|
2
|
+
|
|
3
|
+
import styles from './styles.module.scss';
|
|
4
|
+
|
|
5
|
+
interface MiniSliderProps {
|
|
6
|
+
value: number;
|
|
7
|
+
minValue: number;
|
|
8
|
+
maxValue: number;
|
|
9
|
+
onChange: (newValue: number) => void;
|
|
10
|
+
ariaLabel: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const MiniSlider = (props: MiniSliderProps): React.JSX.Element => {
|
|
14
|
+
const handleOnChange = (event: ChangeEvent<HTMLInputElement>): void => {
|
|
15
|
+
event.preventDefault();
|
|
16
|
+
const newValue = parseFloat(event.target.value);
|
|
17
|
+
props.onChange(newValue);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const ariaValueText = `${Math.round(props.value * 100)}%`;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className={styles.slider} aria-live="polite">
|
|
24
|
+
<div key={props.value} className={styles.slider__announcements}>
|
|
25
|
+
{ariaValueText}
|
|
26
|
+
</div>
|
|
27
|
+
<input
|
|
28
|
+
onChange={handleOnChange}
|
|
29
|
+
type="range"
|
|
30
|
+
min={props.minValue}
|
|
31
|
+
max={props.maxValue}
|
|
32
|
+
value={props.value}
|
|
33
|
+
aria-valuenow={props.value}
|
|
34
|
+
aria-valuemin={props.minValue}
|
|
35
|
+
aria-valuemax={props.maxValue}
|
|
36
|
+
aria-valuetext={ariaValueText}
|
|
37
|
+
aria-label={props.ariaLabel}
|
|
38
|
+
step={0.1}
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default MiniSlider;
|
package/tsconfig.json
ADDED