Skip to content

Commit 1c21ae4

Browse files
authored
Merge pull request #2184 from navikt/redaktorvarsel-expandable
Åpning av accordion og readmore-komponenter ved redaktørvarsel
2 parents b924b24 + a93be89 commit 1c21ae4

File tree

7 files changed

+117
-112
lines changed

7 files changed

+117
-112
lines changed

src/components/_common/accordion/Accordion.tsx

+21-38
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,29 @@ import { classNames } from 'utils/classnames';
1212
import { handleStickyScrollOffset } from 'utils/scroll-to';
1313

1414
import defaultHtml from 'components/_common/parsedHtml/DefaultHtmlStyling.module.scss';
15+
import { useCheckAndOpenAccordionPanel } from 'store/hooks/useCheckAndOpenAccordionPanel';
1516
import styles from './Accordion.module.scss';
1617

1718
type AccordionProps = PartConfigAccordion;
1819
type PanelItem = AccordionProps['accordion'][number];
1920

2021
export const Accordion = ({ accordion }: AccordionProps) => {
21-
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
22+
const itemRefs = useRef(accordion.map(() => React.createRef<HTMLDivElement>()));
2223
const contentProps = usePageContentProps();
2324
const { context } = getDecoratorParams(contentProps);
2425
const { editorView, type } = contentProps;
2526
const [openAccordions, setOpenAccordions] = useState<number[]>([]);
26-
const expandAll = () => {
27-
setOpenAccordions(accordion.map((_, index) => index));
28-
};
29-
const validatePanel = (item: PanelItem) => !!(item.title && item.html);
3027

31-
useShortcuts({ shortcut: Shortcuts.SEARCH, callback: expandAll });
28+
const expandAll = () => setOpenAccordions(accordion.map((_, index) => index));
29+
const validatePanel = (item: PanelItem) => Boolean(item.title && item.html);
3230

33-
const openChangeHandler = (isOpening: boolean, tittel: string, index: number) => {
34-
handleStickyScrollOffset(isOpening, itemRefs.current[index]);
31+
useShortcuts({ shortcut: Shortcuts.SEARCH, callback: expandAll });
3532

36-
if (isOpening) {
37-
setOpenAccordions([...openAccordions, index]);
38-
} else {
39-
setOpenAccordions(openAccordions.filter((i) => i !== index));
40-
}
33+
const handleOpenChange = (isOpening: boolean, tittel: string, index: number) => {
34+
handleStickyScrollOffset(isOpening, itemRefs.current[index].current);
35+
setOpenAccordions((prev) =>
36+
isOpening ? [...prev, index] : prev.filter((i) => i !== index)
37+
);
4138
logAnalyticsEvent(isOpening ? AnalyticsEvents.ACC_EXPAND : AnalyticsEvents.ACC_COLLAPSE, {
4239
tittel,
4340
opprinnelse: 'trekkspill',
@@ -47,20 +44,11 @@ export const Accordion = ({ accordion }: AccordionProps) => {
4744
});
4845
};
4946

50-
useEffect(() => {
51-
if (window.location.toString().includes('expandall=true')) {
52-
expandAll();
53-
return;
54-
}
55-
const anchorHash = window.location.hash || '';
56-
const matchingAccordion = accordion.findIndex(
57-
(item) => validatePanel(item) && item.anchorId === anchorHash.slice(1)
58-
);
59-
if (matchingAccordion !== -1) {
60-
setOpenAccordions([matchingAccordion]);
61-
}
62-
/* eslint-disable-next-line react-hooks/exhaustive-deps */
63-
}, []);
47+
if (itemRefs.current.length !== accordion.length) {
48+
itemRefs.current = accordion.map(() => React.createRef<HTMLDivElement>());
49+
}
50+
51+
useCheckAndOpenAccordionPanel(openAccordions, setOpenAccordions, itemRefs.current, expandAll);
6452

6553
// Show all panels in edit mode, but only valid panels in view mode
6654
const validAccordion = accordion.filter(validatePanel);
@@ -75,21 +63,16 @@ export const Accordion = ({ accordion }: AccordionProps) => {
7563
key={index}
7664
className={styles.item}
7765
open={openAccordions.includes(index)}
78-
onOpenChange={(open) => openChangeHandler(open, item.title, index)}
79-
ref={(el) => {
80-
itemRefs.current[index] = el;
81-
}}
66+
onOpenChange={(open) => handleOpenChange(open, item.title, index)}
67+
ref={itemRefs.current[index]}
8268
tabIndex={-1}
8369
>
8470
<DSAccordion.Header className={styles.header} id={item.anchorId}>
85-
{!isValid && (
86-
<EditorHelp
87-
text={
88-
'Panelet mangler tittel eller innhold. Klikk for å redigere'
89-
}
90-
/>
71+
{!isValid ? (
72+
<EditorHelp text="Panelet mangler tittel eller innhold. Klikk for å redigere" />
73+
) : (
74+
<div className={styles.headerTitle}>{item.title}</div>
9175
)}
92-
{isValid && <div className={styles.headerTitle}>{item.title}</div>}
9376
</DSAccordion.Header>
9477
<DSAccordion.Content className={styles.content}>
9578
<div className={classNames(defaultHtml.html, 'parsedHtml')}>

src/components/_common/expandable/Expandable.tsx

+16-68
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import React, { useEffect, useRef, useState } from 'react';
1+
import React, { useRef, useState } from 'react';
22
import { ExpansionCard } from '@navikt/ds-react';
33
import { BarChartIcon, BriefcaseClockIcon, CalendarIcon, TasklistIcon } from '@navikt/aksel-icons';
44
import { AnalyticsEvents, logAnalyticsEvent } from 'utils/analytics';
55
import { usePageContentProps } from 'store/pageContext';
66
import { getDecoratorParams } from 'utils/decorator-utils';
77
import { innholdsTypeMap } from 'types/content-props/_content-common';
88
import { classNames } from 'utils/classnames';
9-
import { smoothScrollToTarget, handleStickyScrollOffset } from 'utils/scroll-to';
9+
import { handleStickyScrollOffset } from 'utils/scroll-to';
1010
import { Shortcuts, useShortcuts } from 'utils/useShortcuts';
1111
import { ProductDetailType } from 'types/content-props/product-details';
12-
12+
import { useCheckAndOpenPanel } from 'store/hooks/useCheckAndOpenPanel';
1313
import style from './Expandable.module.scss';
1414

1515
type Props = {
@@ -38,17 +38,12 @@ export const Expandable = ({
3838
const contentProps = usePageContentProps();
3939
const { context } = getDecoratorParams(contentProps);
4040

41-
useShortcuts({
42-
shortcut: Shortcuts.SEARCH,
43-
callback: () => {
44-
setIsOpen(true);
45-
},
46-
});
41+
useShortcuts({ shortcut: Shortcuts.SEARCH, callback: () => setIsOpen(true) });
42+
useCheckAndOpenPanel(isOpen, setIsOpen, accordionRef, anchorId);
4743

4844
const toggleExpandCollapse = (isOpening: boolean, tittel: string) => {
4945
setIsOpen(isOpening);
5046
handleStickyScrollOffset(isOpening, accordionRef.current);
51-
5247
logAnalyticsEvent(isOpening ? AnalyticsEvents.ACC_EXPAND : AnalyticsEvents.ACC_COLLAPSE, {
5348
tittel,
5449
opprinnelse: analyticsOriginTag || 'utvidbar tekst',
@@ -58,68 +53,21 @@ export const Expandable = ({
5853
});
5954
};
6055

61-
const checkAndOpenPanel = () => {
62-
if (isOpen) {
63-
return;
64-
}
65-
66-
if (window.location.toString().includes('expandall=true')) {
67-
setIsOpen(true);
68-
return;
69-
}
70-
71-
const targetId = window.location.hash.replace('#', '');
72-
if (!targetId) {
73-
return;
74-
}
75-
76-
if (targetId === anchorId) {
77-
setIsOpen(true);
78-
return;
79-
}
80-
81-
const elementWithId = document.getElementById(targetId);
82-
if (accordionRef.current?.contains(elementWithId)) {
83-
setIsOpen(true);
84-
setTimeout(() => smoothScrollToTarget(targetId), 500);
85-
}
86-
};
87-
88-
const hashChangeHandler = () => {
89-
checkAndOpenPanel();
90-
};
91-
9256
const getHeaderIcon = () => {
93-
if (expandableType === ProductDetailType.PROCESSING_TIMES) {
94-
return <BriefcaseClockIcon aria-hidden className={style.headerIcon} />;
95-
}
96-
if (expandableType === ProductDetailType.PAYOUT_DATES) {
97-
return <CalendarIcon aria-hidden className={style.headerIcon} />;
98-
}
99-
if (expandableType === ProductDetailType.RATES) {
100-
return <BarChartIcon aria-hidden className={style.headerIcon} />;
57+
switch (expandableType) {
58+
case ProductDetailType.PROCESSING_TIMES:
59+
return <BriefcaseClockIcon aria-hidden className={style.headerIcon} />;
60+
case ProductDetailType.PAYOUT_DATES:
61+
return <CalendarIcon aria-hidden className={style.headerIcon} />;
62+
case ProductDetailType.RATES:
63+
return <BarChartIcon aria-hidden className={style.headerIcon} />;
64+
case 'documentation_requirements':
65+
return <TasklistIcon aria-hidden className={style.headerIcon} />;
66+
default:
67+
return null;
10168
}
102-
if (expandableType === 'documentation_requirements') {
103-
return <TasklistIcon aria-hidden className={style.headerIcon} />;
104-
}
105-
return null;
10669
};
10770

108-
useEffect(() => {
109-
window.addEventListener('hashchange', hashChangeHandler);
110-
111-
return () => {
112-
window.removeEventListener('hashchange', hashChangeHandler);
113-
};
114-
115-
// eslint-disable-next-line react-hooks/exhaustive-deps
116-
}, []);
117-
118-
useEffect(() => {
119-
checkAndOpenPanel();
120-
// eslint-disable-next-line react-hooks/exhaustive-deps
121-
}, [anchorId]);
122-
12371
// Adjust appearande in styling if not type was set for this content
12472
// This is the wrong use of this component, but some legacy pages have still to
12573
// be upradet editorial wise.

src/components/_editor-only/warnings/duplicate-ids-warning/DuplicateIdsWarning.module.scss

+8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@
33
.warning {
44
min-width: 400px;
55
background-color: common.$a-white;
6+
7+
&::before {
8+
content: '';
9+
display: block;
10+
height: 100px;
11+
margin-top: -100px;
12+
visibility: hidden;
13+
}
614
}
715

816
.lenkeInline {

src/components/parts/readMorePart/ReadMorePart.module.scss

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
margin-bottom: var(--a-spacing-9);
33

44
:global(.navds-read-more__button) {
5+
z-index: 1;
56
margin-bottom: var(--a-spacing-3);
67
background-color: var(--a-blue-50);
78
padding: var(--a-spacing-2) var(--a-spacing-6) var(--a-spacing-2) 13px;

src/components/parts/readMorePart/ReadMorePart.tsx

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useRef, useState } from 'react';
1+
import React, { useRef, useState } from 'react';
22
import { ReadMore } from '@navikt/ds-react';
33
import { EditorHelp } from 'components/_editor-only/editor-help/EditorHelp';
44
import { ParsedHtml } from 'components/_common/parsedHtml/ParsedHtml';
@@ -13,6 +13,7 @@ import { PartComponentProps, PartType } from 'types/component-props/parts';
1313
import { ProcessedHtmlProps } from 'types/processed-html-props';
1414

1515
import defaultHtml from 'components/_common/parsedHtml/DefaultHtmlStyling.module.scss';
16+
import { useCheckAndOpenPanel } from 'store/hooks/useCheckAndOpenPanel';
1617
import styles from './ReadMorePart.module.scss';
1718

1819
export type PartConfigReadMore = {
@@ -32,11 +33,7 @@ export const ReadMorePart = ({ config }: PartComponentProps<PartType.ReadMore>)
3233
callback: () => setIsOpen(true),
3334
});
3435

35-
useEffect(() => {
36-
if (window.location.toString().includes('expandall=true')) {
37-
setIsOpen(true);
38-
}
39-
}, []);
36+
useCheckAndOpenPanel(isOpen, setIsOpen, divRef);
4037

4138
if (!config?.html || !config.title) {
4239
return <EditorHelp text={'Legg inn tittel og beskrivelse for "les mer".'} type={'error'} />;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useEffect, useCallback } from 'react';
2+
import { smoothScrollToTarget } from 'utils/scroll-to';
3+
4+
export const useCheckAndOpenAccordionPanel = (
5+
openPanels: number[],
6+
setOpenPanels: (indexes: number[]) => void,
7+
refs: React.RefObject<HTMLDivElement>[],
8+
expandAll: () => void
9+
) => {
10+
const checkAndOpenPanels = useCallback(() => {
11+
const targetId = window.location.hash.slice(1);
12+
if (!targetId) return;
13+
14+
if (window.location.search.includes('expandall=true')) {
15+
expandAll();
16+
return;
17+
}
18+
19+
const targetElement = document.getElementById(targetId);
20+
const panelIndex = refs.findIndex((ref) => ref.current?.contains(targetElement));
21+
22+
if (panelIndex !== -1 && !openPanels.includes(panelIndex)) {
23+
setOpenPanels([...openPanels, panelIndex]);
24+
setTimeout(() => smoothScrollToTarget(targetId), 500);
25+
}
26+
}, [openPanels, refs, setOpenPanels, expandAll]);
27+
28+
useEffect(() => {
29+
window.addEventListener('hashchange', checkAndOpenPanels);
30+
return () => window.removeEventListener('hashchange', checkAndOpenPanels);
31+
}, [checkAndOpenPanels]);
32+
33+
useEffect(checkAndOpenPanels, [checkAndOpenPanels]);
34+
};
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useEffect, useCallback } from 'react';
2+
import { smoothScrollToTarget } from 'utils/scroll-to';
3+
4+
export const useCheckAndOpenPanel = (
5+
isOpen: boolean,
6+
setIsOpen: (isOpen: boolean) => void,
7+
ref: React.RefObject<HTMLDivElement>,
8+
anchorId?: string
9+
) => {
10+
const checkAndOpenPanel = useCallback(() => {
11+
if (isOpen) return;
12+
13+
const targetId = window.location.hash.slice(1);
14+
if (!targetId) return;
15+
16+
if (window.location.search.includes('expandall=true') || targetId === anchorId) {
17+
setIsOpen(true);
18+
return;
19+
}
20+
21+
const targetElement = document.getElementById(targetId);
22+
if (ref.current?.contains(targetElement)) {
23+
setIsOpen(true);
24+
setTimeout(() => smoothScrollToTarget(targetId), 500);
25+
}
26+
}, [isOpen, setIsOpen, ref, anchorId]);
27+
28+
useEffect(() => {
29+
window.addEventListener('hashchange', checkAndOpenPanel);
30+
return () => window.removeEventListener('hashchange', checkAndOpenPanel);
31+
}, [checkAndOpenPanel]);
32+
33+
useEffect(checkAndOpenPanel, [checkAndOpenPanel]);
34+
};

0 commit comments

Comments
 (0)