From d9cec436653e80ff4cfad1f92b1d85eac3540a9e Mon Sep 17 00:00:00 2001 From: Padmaja Date: Fri, 16 Aug 2024 14:50:21 +0530 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Modal=20dialog=20for=20video=20tran?= =?UTF-8?q?scripts=20#2236=20(#2398)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Modal transcript - initial version #2236 * ✨ Modal transcript initial version #2236 * Some more * ✨ Add transcript for iframe #2236 * ♿️ Aria labels #2236 --- sanityv3/schemas/index.ts | 2 + sanityv3/schemas/objects/iframe.tsx | 2 + .../objects/iframe/sharedIframeFields.tsx | 5 + sanityv3/schemas/objects/transcript.tsx | 32 ++++++ sanityv3/schemas/objects/videoFile.tsx | 5 + sanityv3/schemas/objects/videoPlayer.tsx | 7 ++ sanityv3/schemas/textSnippets.ts | 5 + web/components/src/Topbar/Topbar.tsx | 1 + web/core/Button/index.tsx | 30 +++++- web/icons/ArrowRight.tsx | 33 +----- web/icons/TransformableIcon.tsx | 50 +++++++++ web/lib/queries/common/pageContentFields.ts | 4 + web/lib/queries/videoPlayerFields.ts | 1 + web/package.json | 6 +- .../shared/TranscriptAndActions.tsx | 65 ++++++++++++ web/pageComponents/shared/VideoPlayer.tsx | 7 +- web/pageComponents/topicPages/IFrame.tsx | 10 +- web/pnpm-lock.yaml | 60 +++++++++-- web/sections/Modal/Modal.tsx | 100 ++++++++++++++++++ web/sections/Modal/index.ts | 2 + web/styles/tailwind.css | 63 +++++++++++ web/tailwind.config.cjs | 18 ++-- web/types/types.ts | 2 + 23 files changed, 448 insertions(+), 62 deletions(-) create mode 100644 sanityv3/schemas/objects/transcript.tsx create mode 100644 web/icons/TransformableIcon.tsx create mode 100644 web/pageComponents/shared/TranscriptAndActions.tsx create mode 100644 web/sections/Modal/Modal.tsx create mode 100644 web/sections/Modal/index.ts diff --git a/sanityv3/schemas/index.ts b/sanityv3/schemas/index.ts index fe11899a0..076e2493b 100644 --- a/sanityv3/schemas/index.ts +++ b/sanityv3/schemas/index.ts @@ -78,6 +78,7 @@ import campaignBanner from './objects/campaignBanner' import gridTeaser from './objects/grid/cellTypes/gridTeaser' import threeColumns from './objects/grid/rowTypes/3columns' import gridColorTheme from './objects/grid/theme' +import transcript from './objects/transcript' const { pageNotFound, @@ -202,6 +203,7 @@ const RemainingSchemas = [ gridTeaser, threeColumns, gridColorTheme, + transcript, ] // Then we give our schema to the builder and provide the result to Sanity diff --git a/sanityv3/schemas/objects/iframe.tsx b/sanityv3/schemas/objects/iframe.tsx index 56cf66708..1c30381f6 100644 --- a/sanityv3/schemas/objects/iframe.tsx +++ b/sanityv3/schemas/objects/iframe.tsx @@ -16,6 +16,7 @@ import { url, height, action, + transcript, } from './iframe/sharedIframeFields' const ingressContentType = configureBlockContent({ @@ -74,6 +75,7 @@ export default { height, description, action, + transcript, { title: 'Background', description: 'Pick a colour for the background. Default is white.', diff --git a/sanityv3/schemas/objects/iframe/sharedIframeFields.tsx b/sanityv3/schemas/objects/iframe/sharedIframeFields.tsx index 82693b5a0..d9d9f21bc 100644 --- a/sanityv3/schemas/objects/iframe/sharedIframeFields.tsx +++ b/sanityv3/schemas/objects/iframe/sharedIframeFields.tsx @@ -104,6 +104,11 @@ export const description = { of: [descriptionContentType], } +export const transcript = { + name: 'transcript', + title: 'Enter transcript if this iframe is a youtube video.', + type: 'transcript', +} export const action = { name: 'action', title: 'Link/action', diff --git a/sanityv3/schemas/objects/transcript.tsx b/sanityv3/schemas/objects/transcript.tsx new file mode 100644 index 000000000..6c4a74681 --- /dev/null +++ b/sanityv3/schemas/objects/transcript.tsx @@ -0,0 +1,32 @@ +import { defineType, defineField } from 'sanity' +import { configureBlockContent } from '../editors/blockContentType' +import CompactBlockEditor from '../components/CompactBlockEditor' + +const blockConfig = { + h2: false, + h3: false, + h4: false, + smallText: false, + internalLink: false, + externalLink: false, + attachment: false, + lists: true, +} + +const blockContentType = configureBlockContent({ ...blockConfig }) + +export default { + name: 'transcript', + title: 'Transcript', + type: 'object', + fields: [ + { + name: 'text', + type: 'array', + components: { + input: CompactBlockEditor, + }, + of: [blockContentType], + }, + ], +} diff --git a/sanityv3/schemas/objects/videoFile.tsx b/sanityv3/schemas/objects/videoFile.tsx index ceeea74f6..e7f8a3149 100644 --- a/sanityv3/schemas/objects/videoFile.tsx +++ b/sanityv3/schemas/objects/videoFile.tsx @@ -16,6 +16,11 @@ export default { type: 'hlsVideo', validation: (Rule: Rule) => Rule.required(), }, + { + name: 'transcript', + title: 'Video Transcript', + type: 'transcript', + }, { name: 'thumbnail', type: 'imageWithAlt', diff --git a/sanityv3/schemas/objects/videoPlayer.tsx b/sanityv3/schemas/objects/videoPlayer.tsx index a3ab6f0dc..91fa592fb 100644 --- a/sanityv3/schemas/objects/videoPlayer.tsx +++ b/sanityv3/schemas/objects/videoPlayer.tsx @@ -70,6 +70,12 @@ export default { collapsed: false, }, }, + { + title: 'Show transcript button', + description: 'Shows transcript from the video asset.', + name: 'showTranscript', + type: 'boolean', + }, { name: 'aspectRatio', type: 'string', @@ -108,6 +114,7 @@ export default { description: 'Set a fixed height in pixels for the video. Note: this will override the aspect ratio setting.', validation: (Rule: Rule) => Rule.positive().greaterThan(0).precision(0), }, + { title: 'Use brand theme for video', description: 'Make play button bigger and brand red.', diff --git a/sanityv3/schemas/textSnippets.ts b/sanityv3/schemas/textSnippets.ts index fb80b7245..0868ad306 100644 --- a/sanityv3/schemas/textSnippets.ts +++ b/sanityv3/schemas/textSnippets.ts @@ -30,6 +30,11 @@ const snippets: textSnippet = { defaultValue: 'Loading...', group: groups.others, }, + read_transcript: { + title: 'Read Transcript', + defaultValue: 'Read transcript', + group: groups.others, + }, menu: { title: 'Menu', defaultValue: 'Menu', diff --git a/web/components/src/Topbar/Topbar.tsx b/web/components/src/Topbar/Topbar.tsx index af90dc9e5..0fd982be9 100644 --- a/web/components/src/Topbar/Topbar.tsx +++ b/web/components/src/Topbar/Topbar.tsx @@ -35,6 +35,7 @@ export const Topbar = ({ children, ...rest }: HTMLAttributes) => let currentScrollPos = window.pageYOffset // Fix for iOS to avoid negative scroll positions if (currentScrollPos < 0) currentScrollPos = 0 + setIsVisible( (prevScrollPos > currentScrollPos && prevScrollPos - currentScrollPos > height) || currentScrollPos < prevScrollPos || diff --git a/web/core/Button/index.tsx b/web/core/Button/index.tsx index a394d93a3..f1fab7276 100644 --- a/web/core/Button/index.tsx +++ b/web/core/Button/index.tsx @@ -5,8 +5,10 @@ import { twMerge } from 'tailwind-merge' export const commonButtonStyling = ` w-fit text-sm -px-5 -py-3 +px-3 +py-2 +lg:px-5 +lg:py-3 rounded-md focus:outline-none focus-visible:envis-outline @@ -18,7 +20,7 @@ items-center gap-3 ` -export type Variants = 'contained' | 'outlined' | 'ghost' +export type Variants = 'contained' | 'outlined' | 'ghost' | 'contained-secondary' | 'outlined-secondary' /** Use for common button styling in Button,IconButton, Link/ButtonLink */ export const getVariant = (variant: Variants): string => { @@ -47,6 +49,28 @@ export const getVariant = (variant: Variants): string => { dark:hover:bg-white-transparent dark:focus-visible:outline-white-100 ` + case 'outlined-secondary': + return ` + border + border-north-sea-100 + text-black-80 + hover:bg-slate-blue-100 + hover:text-white-100 + focus:outline-none + focus-visible:outline-slate-blue-95 + dark:text-white-100 + dark:border-white-100 + dark:hover:bg-white-transparent + dark:focus-visible:outline-white-100 + ` + case 'contained-secondary': + return `bg-slate-blue-95 + text-white-100 + hover:bg-slate-blue-100 + hover:text-white-100 + focus:outline-none + focus-visible:outline-slate-blue-95 + ` case 'contained': default: return `bg-norwegian-woods-100 diff --git a/web/icons/ArrowRight.tsx b/web/icons/ArrowRight.tsx index 26184375c..4c09f4d46 100644 --- a/web/icons/ArrowRight.tsx +++ b/web/icons/ArrowRight.tsx @@ -1,5 +1,6 @@ import { forwardRef, Ref, SVGProps } from 'react' import { arrow_forward } from '@equinor/eds-icons' +import { TransformableIcon } from './TransformableIcon' export type ArrowRightProps = { /** Size, use if you need large icon resolutions @@ -10,35 +11,7 @@ export type ArrowRightProps = { ref?: Ref } & SVGProps -export const ArrowRight = forwardRef(function ArrowRight( - { size = 24, className = '', ...rest }, - ref, -) { - let icon = arrow_forward - if (size < 24) { - // fallback to normal icon if small is not made yet - icon = icon.sizes?.small || icon - } - return ( - - {Array.isArray(icon.svgPathData) ? ( - icon.svgPathData.map((pathData) => { - return - }) - ) : ( - - )} - - ) +export const ArrowRight = forwardRef(function ArrowRight({ ...rest }, ref) { + return }) export default ArrowRight diff --git a/web/icons/TransformableIcon.tsx b/web/icons/TransformableIcon.tsx new file mode 100644 index 000000000..fd704ffe1 --- /dev/null +++ b/web/icons/TransformableIcon.tsx @@ -0,0 +1,50 @@ +import { forwardRef, Ref, SVGProps } from 'react' +import { IconData } from '@equinor/eds-icons' + +/** + * Use this to transform an icon for example arrow right can be rotated to + * 45 deg/-90 deg to use it as external link icon or download icon. + * Similarly the + and close + */ +export type TransformableIconProps = { + iconData: IconData + /** Size, use if you need large icon resolutions + * @default 24 + */ + size?: 16 | 18 | 24 | 32 | 40 | 48 + /** @ignore */ + ref?: Ref +} & SVGProps + +export const TransformableIcon = forwardRef(function ArrowRight( + { iconData, size = 24, className = '', ...rest }, + ref, +) { + let icon = iconData + if (size < 24) { + // fallback to normal icon if small is not made yet + icon = icon.sizes?.small || icon + } + return ( + + {Array.isArray(icon.svgPathData) ? ( + icon.svgPathData.map((pathData) => { + return + }) + ) : ( + + )} + + ) +}) +export default TransformableIcon diff --git a/web/lib/queries/common/pageContentFields.ts b/web/lib/queries/common/pageContentFields.ts index 3d48d2019..1dc561328 100644 --- a/web/lib/queries/common/pageContentFields.ts +++ b/web/lib/queries/common/pageContentFields.ts @@ -215,6 +215,10 @@ _type == "keyNumbers" =>{ ..., ${markDefs}, }, + "transcript":transcript.text[]{ + ..., + ${markDefs}, + }, frameTitle, "action": action[0]{ ${linkSelectorFields}, diff --git a/web/lib/queries/videoPlayerFields.ts b/web/lib/queries/videoPlayerFields.ts index d604cc699..f120f07da 100644 --- a/web/lib/queries/videoPlayerFields.ts +++ b/web/lib/queries/videoPlayerFields.ts @@ -26,6 +26,7 @@ export const videoPlayerFields = /* groq */ ` autoPlay, muted, }, + defined(showTranscript) && showTranscript => { "transcript": videoFile->transcript.text}, "designOptions": { "aspectRatio": coalesce(aspectRatio, '16:9'), height, diff --git a/web/package.json b/web/package.json index 59e07b963..2b9356332 100644 --- a/web/package.json +++ b/web/package.json @@ -41,6 +41,7 @@ "@sanity/webhook": "^2.0.0", "@types/easy-soap-request": "^4.1.1", "@types/uuid": "^8.3.4", + "@types/react-transition-group": "^4.4.10", "@types/xml2js": "^0.4.11", "algoliasearch": "^4.16.0", "date-fns": "^2.29.3", @@ -80,7 +81,8 @@ "swr": "^1.3.0", "tailwind-merge": "^2.2.1", "uuid": "^9.0.0", - "xml2js": "^0.6.0" + "xml2js": "^0.6.0", + "react-transition-group": "^4.4.5" }, "devDependencies": { "@algolia/client-search": "^4.16.0", @@ -114,4 +116,4 @@ "vite": "^5.2.10", "webpack": "^5.88.2" } -} \ No newline at end of file +} diff --git a/web/pageComponents/shared/TranscriptAndActions.tsx b/web/pageComponents/shared/TranscriptAndActions.tsx new file mode 100644 index 000000000..9cc230e7e --- /dev/null +++ b/web/pageComponents/shared/TranscriptAndActions.tsx @@ -0,0 +1,65 @@ +import { LinkData } from '../../types' +import { getUrlFromAction } from '../../common/helpers' +import { useState } from 'react' +import { PortableTextBlock } from '@portabletext/types' +import { ButtonLink } from '@core/Link' +import { commonButtonStyling, getVariant } from '@core/Button' +import { getLocaleFromName } from '../../lib/localization' +import Modal from '@sections/Modal/Modal' +import RichText from './portableText/RichText' +import { add_circle_filled } from '@equinor/eds-icons' +import { twMerge } from 'tailwind-merge' +import { TransformableIcon } from '../../icons/TransformableIcon' +import { useIntl } from 'react-intl' +import { title } from 'process' + +type TranscriptAndActionsProps = { + className?: string + action?: LinkData + transcript?: PortableTextBlock[] + ariaTitle: string +} +const TranscriptAndActions = ({ action, transcript, className, ariaTitle }: TranscriptAndActionsProps) => { + const [isOpen, setIsOpen] = useState(false) + const actionUrl = action ? getUrlFromAction(action) : '' + const intl = useIntl() + const readTranscript = intl.formatMessage({ id: 'read_transcript', defaultMessage: 'Read transcript' }) + const handleOpen = () => { + setIsOpen(true) + } + const handleClose = () => { + setIsOpen(false) + } + return ( +
+ {action && action.label && ( + + {action.label} + + )} + + {transcript && ( + <> + + + + + + )} +
+ ) +} +export default TranscriptAndActions diff --git a/web/pageComponents/shared/VideoPlayer.tsx b/web/pageComponents/shared/VideoPlayer.tsx index 9fe356c9e..9d5ee2df1 100644 --- a/web/pageComponents/shared/VideoPlayer.tsx +++ b/web/pageComponents/shared/VideoPlayer.tsx @@ -13,7 +13,7 @@ import IngressText from './portableText/IngressText' import { VideoJS } from '@components/VideoJsPlayer' import { twMerge } from 'tailwind-merge' import { Heading } from '@core/Typography' -import CallToActions from '@sections/CallToActions' +import TranscriptAndActions from './TranscriptAndActions' const DynamicVideoJsComponent = dynamic>( () => import('../../components/src/VideoJsPlayer').then((mod) => mod.VideoJS), @@ -106,8 +106,9 @@ export const VideoJsComponent = ({ } const VideoPlayer = ({ anchor, data, className }: { data: VideoPlayerData; anchor?: string; className?: string }) => { - const { title, ingress, action, video, videoControls, designOptions } = data + const { title, ingress, action, video, videoControls, designOptions, transcript } = data const { width } = designOptions + return (
{title && } {ingress && } - {action && action.label && } +
) diff --git a/web/pageComponents/topicPages/IFrame.tsx b/web/pageComponents/topicPages/IFrame.tsx index 03a5bad4c..f4fa246fd 100644 --- a/web/pageComponents/topicPages/IFrame.tsx +++ b/web/pageComponents/topicPages/IFrame.tsx @@ -2,11 +2,11 @@ import styled from 'styled-components' import type { IFrameData } from '../../types/types' import { BackgroundContainer, FigureCaption } from '@components' import CoreIFrame from '../shared/iframe/IFrame' -import { ButtonLink } from '../shared/ButtonLink' import IngressText from '../shared/portableText/IngressText' import TitleText from '../shared/portableText/TitleText' import RichText from '../shared/portableText/RichText' import { twMerge } from 'tailwind-merge' +import TranscriptAndActions from '../../pageComponents/shared/TranscriptAndActions' const StyledHeading = styled(TitleText)` padding: 0 0 var(--space-large) 0; @@ -17,17 +17,13 @@ const Figure = styled.figure` margin: 0; ` -const StyledButtonLink = styled(ButtonLink)` - margin-top: var(--space-xLarge); -` - const Ingress = styled.div` margin-bottom: var(--space-large); ` const IFrame = ({ anchor, - data: { title, ingress, frameTitle, url, description, cookiePolicy = 'none', designOptions, action }, + data: { title, ingress, frameTitle, url, description, cookiePolicy = 'none', designOptions, action, transcript }, className, ...rest }: { @@ -71,7 +67,7 @@ const IFrame = ({ hasSectionTitle={!!title} /> )} - {action && action.label && } + ) diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index a7a24468a..b036e3023 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: '@algolia/cache-browser-local-storage': specifier: ^4.22.1 @@ -67,6 +71,9 @@ dependencies: '@types/easy-soap-request': specifier: ^4.1.1 version: 4.1.1 + '@types/react-transition-group': + specifier: ^4.4.10 + version: 4.4.10 '@types/uuid': specifier: ^8.3.4 version: 8.3.4 @@ -160,6 +167,9 @@ dependencies: react-is: specifier: ^18.1.0 version: 18.1.0 + react-transition-group: + specifier: ^4.4.5 + version: 4.4.5(react-dom@18.2.0)(react@18.2.0) react-twitter-embed: specifier: ^4.0.4 version: 4.0.4(react-dom@18.2.0)(react@18.2.0) @@ -388,7 +398,7 @@ packages: resolution: {integrity: sha512-IlYgIaCUEkz9ezNbwugwKv991oOHhveyq6nzL0F1jDzg1p3q5Yj/vO4KpNG910r2dwGCG3nEm5GtChcLnarhFA==} dependencies: '@algolia/ui-components-shared': 1.2.1 - '@babel/runtime': 7.23.9 + '@babel/runtime': 7.24.4 dev: false /@algolia/ui-components-shared@1.2.1: @@ -2904,7 +2914,7 @@ packages: /@chakra-ui/styled-system@2.4.0: resolution: {integrity: sha512-G4HpbFERq4C1cBwKNDNkpCiliOICLXjYwKI/e/6hxNY+GlPxt8BCzz3uhd3vmEoG2vRM4qjidlVjphhWsf6vRQ==} dependencies: - csstype: 3.1.2 + csstype: 3.1.3 lodash.mergewith: 4.6.2 dev: false @@ -3148,7 +3158,7 @@ packages: resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} dependencies: '@babel/helper-module-imports': 7.22.5 - '@babel/runtime': 7.23.9 + '@babel/runtime': 7.24.4 '@emotion/hash': 0.9.1 '@emotion/memoize': 0.8.1 '@emotion/serialize': 1.1.2 @@ -3227,7 +3237,7 @@ packages: '@emotion/memoize': 0.8.1 '@emotion/unitless': 0.8.1 '@emotion/utils': 1.2.1 - csstype: 3.1.2 + csstype: 3.1.3 dev: false /@emotion/sheet@1.2.2: @@ -6823,6 +6833,12 @@ packages: '@types/react': 18.2.17 dev: false + /@types/react-transition-group@4.4.10: + resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==} + dependencies: + '@types/react': 18.2.17 + dev: false + /@types/react@18.0.9: resolution: {integrity: sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw==} dependencies: @@ -6835,7 +6851,7 @@ packages: dependencies: '@types/prop-types': 15.7.5 '@types/scheduler': 0.16.3 - csstype: 3.1.2 + csstype: 3.1.3 dev: false /@types/scheduler@0.16.3: @@ -7567,6 +7583,7 @@ packages: /bare-events@2.2.2: resolution: {integrity: sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==} + requiresBuild: true dev: false optional: true @@ -7582,11 +7599,13 @@ packages: /bare-os@2.3.0: resolution: {integrity: sha512-oPb8oMM1xZbhRQBngTgpcQ5gXw6kjOaRsSWsIeNyRxGed2w/ARyP7ScBYpWR1qfX2E5rS3gBw6OWcSQo+s+kUg==} + requiresBuild: true dev: false optional: true /bare-path@2.1.2: resolution: {integrity: sha512-o7KSt4prEphWUHa3QUwCxUI00R86VdjiuxmJK0iNVDHYPGo+HsDaVCnqCmPbf/MiW1ok8F4p3m8RTHlWk8K2ig==} + requiresBuild: true dependencies: bare-os: 2.3.0 dev: false @@ -7594,6 +7613,7 @@ packages: /bare-stream@1.0.0: resolution: {integrity: sha512-KhNUoDL40iP4gFaLSsoGE479t0jHijfYdIcxRn/XtezA2BaUD0NRf/JGRpsMq6dMNM+SrCrB0YSSo/5wBY4rOQ==} + requiresBuild: true dependencies: streamx: 2.16.1 dev: false @@ -8511,6 +8531,13 @@ packages: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} dev: true + /dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dependencies: + '@babel/runtime': 7.24.4 + csstype: 3.1.3 + dev: false + /dom-walk@0.1.2: resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} dev: false @@ -12222,7 +12249,7 @@ packages: algoliasearch: '>= 3.1 < 5' react: '>= 16.8.0 < 19' dependencies: - '@babel/runtime': 7.23.9 + '@babel/runtime': 7.24.4 algoliasearch: 4.16.0 algoliasearch-helper: 3.16.2(algoliasearch@4.16.0) instantsearch.js: 4.65.0(algoliasearch@4.16.0) @@ -12376,6 +12403,20 @@ packages: tslib: 2.6.2 dev: false + /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + dependencies: + '@babel/runtime': 7.24.4 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-twitter-embed@4.0.4(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-2JIL7qF+U62zRzpsh6SZDXNI3hRNVYf5vOZ1WRcMvwKouw+xC00PuFaD0aEp2wlyGaZ+f4x2VvX+uDadFQ3HVA==} engines: {node: '>=10'} @@ -13170,6 +13211,7 @@ packages: /streamx@2.16.1: resolution: {integrity: sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==} + requiresBuild: true dependencies: fast-fifo: 1.3.2 queue-tick: 1.0.1 @@ -14494,7 +14536,7 @@ packages: resolution: {integrity: sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==} engines: {node: '>=10'} dependencies: - '@babel/runtime': 7.23.9 + '@babel/runtime': 7.24.4 '@types/lodash': 4.14.195 lodash: 4.17.21 lodash-es: 4.17.21 @@ -14515,7 +14557,3 @@ packages: /zod@3.23.4: resolution: {integrity: sha512-/AtWOKbBgjzEYYQRNfoGKHObgfAZag6qUJX1VbHo2PRBgS+wfWagEY2mizjfyAPcGesrJOcx/wcl0L9WnVrHFw==} dev: false - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/web/sections/Modal/Modal.tsx b/web/sections/Modal/Modal.tsx new file mode 100644 index 000000000..296dff9ff --- /dev/null +++ b/web/sections/Modal/Modal.tsx @@ -0,0 +1,100 @@ +/* eslint-disable jsx-a11y/no-noninteractive-element-interactions*/ +import { add_circle_filled } from '@equinor/eds-icons' +import { TransformableIcon } from '../../icons/TransformableIcon' +import { useEffect, useRef, useState } from 'react' +import { CSSTransition } from 'react-transition-group' + +export type ModalProps = { + isOpen: boolean + onClose: () => void + title: string + children: React.ReactNode +} + +const Modal: React.FC = ({ isOpen, onClose, title, children }) => { + const modalRef = useRef(null) + const closeButtonRef = useRef(null) + const nodeRef = useRef(null) + const [open, setOpen] = useState(false) + + useEffect(() => { + if (open) { + const previouslyFocusedElement = document.activeElement as HTMLElement + closeButtonRef.current?.focus() + const scrollBarWidth = window.innerWidth - document.body.offsetWidth + document.body.style.margin = `0px ${scrollBarWidth}px 0px 0px` + document.body.style.overflow = 'hidden' + + return () => { + document.body.style.margin = `` + document.body.style.overflow = '' + previouslyFocusedElement?.focus() + } + } + }, [open]) + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Escape') { + onClose() + } + + if (event.key === 'Tab') { + const focusableModalElements = modalRef.current?.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', + ) + const firstElement = focusableModalElements?.[0] as HTMLElement + const lastElement = focusableModalElements?.[focusableModalElements.length - 1] as HTMLElement + + if (event.shiftKey) { + if (document.activeElement === firstElement) { + event.preventDefault() + lastElement?.focus() + } + } else { + if (document.activeElement === lastElement) { + event.preventDefault() + firstElement?.focus() + } + } + } + } + + return ( + setOpen(true)} + onExited={() => setOpen(false)} + > +
+
+
+
+ +
+
{children}
+
+
+
+
+ ) +} + +export default Modal diff --git a/web/sections/Modal/index.ts b/web/sections/Modal/index.ts new file mode 100644 index 000000000..755bf0885 --- /dev/null +++ b/web/sections/Modal/index.ts @@ -0,0 +1,2 @@ +//"use client"; +export { default as Modal, type ModalProps } from './Modal' diff --git a/web/styles/tailwind.css b/web/styles/tailwind.css index bca17a1b3..6d0a11e0b 100644 --- a/web/styles/tailwind.css +++ b/web/styles/tailwind.css @@ -160,4 +160,67 @@ .box-shadow-crisp-interact { box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.08) 0px 3px 4px 0px; } + + .modal-enter { + opacity: 0; + z-index: 11; + > div { + > div { + opacity: 0; + clip-path: inset(10% round 0.5rem); + transform: translateY(40vh); + } + } + } + .modal-enter-done { + opacity: 1; + z-index: 11; + --transition-duration: 0.4s; + --transition-easing: cubic-bezier(0.45, 0, 0.55, 1); + transition: z-index 0s linear var(--transition-duration), + opacity calc(var(--transition-duration) * 2 / 3) var(--transition-easing) calc(var(--transition-duration) / 3); + > div { + > div { + clip-path: inset(0 round 0.5rem); + transform: none; + opacity: 1; + transition: opacity calc(var(--transition-duration) / 2) var(--transition-easing) + calc(var(--transition-duration) / 2), + transform var(--transition-duration) var(--transition-easing), + clip-path var(--transition-duration) var(--transition-easing); + } + } + } + + .modal-exit-active { + opacity: 1; + z-index: 11; + > div { + > div { + clip-path: inset(0 round 0.5rem); + transform: none; + opacity: 1; + } + } + } + .modal-exit { + opacity: 0; + z-index: -1; + --transition-duration: 0.4s; + --transition-easing: cubic-bezier(0.45, 0, 0.55, 1); + + transition: z-index 0s linear var(--transition-duration), + opacity calc(var(--transition-duration) * 2 / 3) var(--transition-easing) calc(var(--transition-duration) / 3); + > div { + > div { + opacity: 0; + clip-path: inset(10% round 0.5rem); + transform: translateY(40vh); + transition: opacity calc(var(--transition-duration) / 2) var(--transition-easing) + calc(var(--transition-duration) / 2), + transform var(--transition-duration) var(--transition-easing), + clip-path var(--transition-duration) var(--transition-easing); + } + } + } } diff --git a/web/tailwind.config.cjs b/web/tailwind.config.cjs index 1c9922fdf..b554a5c6a 100644 --- a/web/tailwind.config.cjs +++ b/web/tailwind.config.cjs @@ -21,6 +21,7 @@ module.exports = { './sections/**/*.{js,ts,tsx}', './icons/**/*.{js,ts,tsx}', ], + safelist: ['modal-enter', 'modal-enter-done', 'modal-exit-active', 'modal-exit'], /* Now instead of dark:{class} classes being applied based on prefers-color-scheme, they will be applied whenever the dark class is present earlier in the HTML tree. @@ -121,7 +122,7 @@ module.exports = { blue: { //--mid-blue //--bg-mid-blue - //north-sea-80 + //north-sea-70 50: colors.blue[50], }, orange: { @@ -152,11 +153,12 @@ module.exports = { }, 'north-sea': { 100: '#243746', - 90: '#2A4D74', - 80: '#49709C', - 70: '#7294BB', - 60: '#A8C3DB', - 50: '#DFF5FF', + 90: '#051b33', + 80: '#2A4D74', + 70: '#49709C', + 60: '#7294BB', + 50: '#A8C3DB', + 40: '#DFF5FF', }, 'norwegian-woods': { 100: '#007079', @@ -180,6 +182,10 @@ module.exports = { 50: '#B5C7C9', 40: '#E3EDEA', }, + + 'modal-background': { + 100: 'hsla(212, 82%, 11%, 1)', + }, }), boxShadowColor: { 'moss-green-50': '190deg 9% 67%', diff --git a/web/types/types.ts b/web/types/types.ts index 4dd7a8540..852d4d553 100644 --- a/web/types/types.ts +++ b/web/types/types.ts @@ -535,6 +535,7 @@ export type IFrameData = { title?: PortableTextBlock[] ingress?: PortableTextBlock[] description?: PortableTextBlock[] + transcript?: PortableTextBlock[] action?: LinkData frameTitle: string url: string @@ -739,6 +740,7 @@ export type VideoPlayerData = { title?: PortableTextBlock[] ingress?: PortableTextBlock[] action?: LinkData + transcript?: PortableTextBlock[] } export type VideoPlayerCarouselData = {