Skip to content

Commit 6f9abed

Browse files
Merge branch 'main' into trustarc-scripts
2 parents 2a987ab + 609412f commit 6f9abed

File tree

70 files changed

+7072
-857
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+7072
-857
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { useEffect, useState, useCallback, useRef } from 'react';
2+
import LinkIcon from '../svg/LinkIcon';
3+
4+
declare const mixpanel: { track: (event: string, props?: Record<string, unknown>) => void };
5+
6+
type ExtendedAccordionProps = {
7+
title: string;
8+
id: string;
9+
section?: string;
10+
children: React.ReactNode;
11+
}
12+
13+
function slugify(text: string): string {
14+
return text.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').trim();
15+
}
16+
17+
export default function ExtendedAccordion({ title, id: idProp, section, children }: ExtendedAccordionProps) {
18+
const id = idProp || slugify(title);
19+
const [isOpen, setIsOpen] = useState(false);
20+
const [copied, setCopied] = useState(false);
21+
const contentRef = useRef<HTMLDivElement>(null);
22+
23+
// Auto-open if URL hash matches on mount
24+
useEffect(() => {
25+
if (typeof window !== 'undefined' && window.location.hash === `#${id}`) {
26+
setIsOpen(true);
27+
setTimeout(() => {
28+
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
29+
}, 100);
30+
}
31+
}, [id]);
32+
33+
// Set hidden="until-found" on the inner content div when closed.
34+
// This makes text searchable via cmd+f in Chrome/Firefox.
35+
// In Safari (no until-found support), display:block !important in CSS keeps
36+
// the grid trick working so content is still findable via cmd+f.
37+
useEffect(() => {
38+
const el = contentRef.current;
39+
if (!el) return;
40+
if (!isOpen) {
41+
el.setAttribute('hidden', 'until-found');
42+
} else {
43+
el.removeAttribute('hidden');
44+
}
45+
}, [isOpen]);
46+
47+
// Listen for beforematch: fires in Chrome/Firefox when browser find
48+
// highlights text inside a hidden="until-found" element.
49+
useEffect(() => {
50+
const el = contentRef.current;
51+
if (!el) return;
52+
const handleBeforeMatch = () => {
53+
setIsOpen(true);
54+
const url = new URL(window.location.href);
55+
url.hash = id;
56+
window.history.replaceState(null, '', url.toString());
57+
};
58+
el.addEventListener('beforematch', handleBeforeMatch);
59+
return () => el.removeEventListener('beforematch', handleBeforeMatch);
60+
}, [id]);
61+
62+
const handleToggle = useCallback(() => {
63+
const opening = !isOpen;
64+
setIsOpen(opening);
65+
if (opening) {
66+
if (typeof mixpanel !== 'undefined') {
67+
mixpanel.track("[Docs] FAQ Item Expanded", {
68+
"faq-question": title,
69+
...(section && { "faq-section": section }),
70+
});
71+
}
72+
const url = new URL(window.location.href);
73+
url.hash = id;
74+
window.history.replaceState(null, '', url.toString());
75+
} else {
76+
const url = new URL(window.location.href);
77+
url.hash = '';
78+
window.history.replaceState(null, '', url.toString());
79+
}
80+
}, [isOpen, title, section, id]);
81+
82+
const handleCopyLink = useCallback(async (e: React.MouseEvent | React.KeyboardEvent) => {
83+
e.stopPropagation();
84+
const url = new URL(window.location.href);
85+
url.hash = id;
86+
const urlString = url.toString();
87+
try {
88+
await navigator.clipboard.writeText(urlString);
89+
} catch {
90+
const ta = document.createElement('textarea');
91+
ta.value = urlString;
92+
ta.style.position = 'fixed';
93+
ta.style.left = '-999999px';
94+
document.body.appendChild(ta);
95+
ta.select();
96+
document.execCommand('copy');
97+
document.body.removeChild(ta);
98+
}
99+
setCopied(true);
100+
setTimeout(() => setCopied(false), 2000);
101+
}, [id]);
102+
103+
return (
104+
<div id={id} className={`extended-accordion${isOpen ? ' open' : ''}`}>
105+
<button
106+
className="accordion-header"
107+
onClick={handleToggle}
108+
aria-expanded={isOpen}
109+
aria-controls={`${id}-body`}
110+
>
111+
<span className="accordion-title-row">
112+
<strong className="accordion-title">{title}</strong>
113+
<span
114+
role="button"
115+
tabIndex={0}
116+
className={`accordion-anchor-link${copied ? ' copied' : ''}`}
117+
onClick={handleCopyLink}
118+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleCopyLink(e); }}
119+
aria-label={`Copy link to ${title}`}
120+
title={copied ? 'Copied!' : 'Copy link to this section'}
121+
>
122+
<LinkIcon />
123+
</span>
124+
</span>
125+
<svg
126+
viewBox="0 0 24 24"
127+
stroke="currentColor"
128+
fill="none"
129+
strokeWidth="2"
130+
height="18"
131+
className={`accordion-chevron${isOpen ? ' rotated' : ''}`}
132+
>
133+
<path d="M9 5l7 7-7 7" strokeLinecap="round" strokeLinejoin="round" />
134+
</svg>
135+
</button>
136+
<div
137+
id={`${id}-body`}
138+
className="accordion-body"
139+
role="region"
140+
aria-labelledby={id}
141+
>
142+
<div ref={contentRef} className="accordion-content">
143+
{children}
144+
</div>
145+
</div>
146+
</div>
147+
);
148+
}

components/ExtendedTabs/ExtendedTabs.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,17 @@ export default function ExtendedTabs(props: ExtendedTabsType) {
6767
const tabItemsWithAnchors = Object.entries(props.urlToItemsMap).map(([key, label], index) => (
6868
<span key={key} className="tab-label-with-anchor">
6969
<span className="tab-label-text">{label}</span>
70-
<button
70+
<span
71+
role="button"
72+
tabIndex={0}
7173
className={`tab-anchor-link ${copiedIndex === index ? 'copied' : ''}`}
7274
onClick={(e) => handleCopyLink(key, index, e)}
75+
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleCopyLink(key, index, e as unknown as React.MouseEvent); }}
7376
aria-label={`Copy link to ${label} tab`}
7477
title={copiedIndex === index ? 'Copied!' : 'Copy link to this tab'}
7578
>
7679
<LinkIcon />
77-
</button>
80+
</span>
7881
</span>
7982
));
8083

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,36 @@
11
@use "../../pages/theme/colors.scss" as colors;
22

33
.signUpButton {
4-
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
5-
background: linear-gradient(1deg, colors.$purple100 0.97%, colors.$purple50 96.96%);
4+
background: linear-gradient(1deg, #{colors.$purple100} 0.97%, #{colors.$purple50} 96.96%);
65
font-weight: 500;
76
font-size: 1rem;
87
line-height: 1.5rem;
98
color: colors.$white;
109
padding: 10px 22px;
1110
text-wrap: nowrap;
1211
border-radius: 100px;
12+
user-select: none;
13+
border: 1px solid #1b0b3b;
1314

14-
&:hover {
15-
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
16-
background-color: colors.$purple100;
15+
// CSS can't interpolate between two gradients, so we layer the hover gradient
16+
// behind the text via ::after and transition its opacity instead.
17+
// isolation: isolate creates a stacking context so z-index: -1 on ::after
18+
// sits above this element's own background but below its inline text content.
19+
position: relative;
20+
isolation: isolate;
21+
22+
&::after {
23+
content: '';
24+
position: absolute;
25+
inset: 0;
26+
border-radius: inherit;
27+
background: linear-gradient(1deg, #{colors.$purple140} 0.97%, #{colors.$purple100} 96.96%);
28+
opacity: 0;
29+
transition: opacity 0.2s ease-in-out;
30+
z-index: -1;
31+
}
32+
33+
&:hover::after {
34+
opacity: 1;
1735
}
1836
}

0 commit comments

Comments
 (0)