mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 00:55:25 +00:00
fix: popup styles and jankiness (#6787)
* feat: greatly improve the jankiness * fix: US variant of consent popup * fix: US cmp in website and styles for US cmp * remove incognito * fix: flashing box in top left --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -487,3 +487,109 @@ input {
|
||||
background-color: transparent;
|
||||
color: hsl(145, 78%, 28%);
|
||||
}
|
||||
|
||||
#qc-cmp2-usp {
|
||||
background: var(--color-raised-bg) !important;
|
||||
border: 1px solid var(--color-divider) !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
box-shadow: var(--shadow-floating) !important;
|
||||
color: var(--color-base) !important;
|
||||
font-family: var(--font-standard) !important;
|
||||
max-width: 660px;
|
||||
|
||||
.qc-usp-ui-content,
|
||||
.qc-usp-ui-form-content,
|
||||
.qc-usp-container {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
p,
|
||||
label,
|
||||
.qc-usp-action-description {
|
||||
color: var(--color-base) !important;
|
||||
font-family: var(--font-standard) !important;
|
||||
}
|
||||
|
||||
.qc-usp-title,
|
||||
.qc-cmp2-list-item-title {
|
||||
color: var(--color-contrast) !important;
|
||||
font-family: var(--font-standard) !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.qc-usp-title {
|
||||
font-size: 1.25rem !important;
|
||||
}
|
||||
|
||||
a,
|
||||
.qc-usp-alt-action {
|
||||
color: var(--color-link) !important;
|
||||
}
|
||||
|
||||
.qc-cmp2-list-item {
|
||||
background: var(--color-button-bg) !important;
|
||||
border: 1px solid var(--color-divider) !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
}
|
||||
|
||||
.qc-cmp2-list-item-header {
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
color: var(--color-contrast) !important;
|
||||
}
|
||||
|
||||
.qc-cmp2-list-item-header svg {
|
||||
color: var(--color-base) !important;
|
||||
}
|
||||
|
||||
.qc-cmp2-toggle {
|
||||
background: var(--surface-3) !important;
|
||||
border-color: var(--color-divider) !important;
|
||||
}
|
||||
|
||||
.qc-cmp2-toggle .toggle {
|
||||
background: var(--color-contrast) !important;
|
||||
}
|
||||
|
||||
.qc-cmp2-toggle .text {
|
||||
color: var(--color-contrast) !important;
|
||||
}
|
||||
|
||||
.qc-cmp2-toggle[aria-checked='true'] {
|
||||
background: var(--color-brand) !important;
|
||||
border-color: var(--color-brand) !important;
|
||||
}
|
||||
|
||||
.qc-cmp2-toggle[aria-checked='true'] .text {
|
||||
color: var(--color-accent-contrast) !important;
|
||||
}
|
||||
|
||||
button[mode='primary'] {
|
||||
background: var(--color-brand) !important;
|
||||
border: 0 !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
color: var(--color-accent-contrast) !important;
|
||||
font-family: var(--font-standard) !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.qc-usp-close-icon {
|
||||
border: 0 !important;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 6 6 18'/%3E%3Cpath d='m6 6 12 12'/%3E%3C/svg%3E")
|
||||
center / 1.5rem 1.5rem no-repeat;
|
||||
}
|
||||
|
||||
a:focus-visible,
|
||||
button:focus-visible {
|
||||
outline: 2px solid var(--color-brand) !important;
|
||||
outline-offset: 2px !important;
|
||||
}
|
||||
|
||||
.qc-usp-ui-content {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
.qc-usp-ui-content .qc-usp-ui-form-content {
|
||||
border: 1px solid transparent !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { onBeforeUnmount, onMounted } from 'vue'
|
||||
|
||||
type ConsentAction = 'accept' | 'reject' | 'manage'
|
||||
type ConsentVariant = 'tcf' | 'usp'
|
||||
|
||||
interface TcfData {
|
||||
eventStatus?: string
|
||||
@@ -17,6 +18,19 @@ interface TcfData {
|
||||
type TcfCallback = (data: TcfData, success: boolean) => void
|
||||
type TcfApi = (command: string, version: number, callback: TcfCallback, parameter?: unknown) => void
|
||||
|
||||
interface UspControls {
|
||||
toggles: HTMLButtonElement[]
|
||||
confirmButton: HTMLButtonElement
|
||||
}
|
||||
|
||||
interface GppData {
|
||||
eventName?: string
|
||||
listenerId?: number
|
||||
}
|
||||
|
||||
type GppCallback = (data: GppData, success: boolean) => void
|
||||
type GppApi = (command: string, callback: GppCallback, parameter?: unknown) => void
|
||||
|
||||
const CMP_HIDDEN_CLASS = 'modrinth-cmp-summary-hidden'
|
||||
const notificationManager = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
@@ -47,8 +61,17 @@ const messages = defineMessages({
|
||||
|
||||
let notificationId: WebNotification['id'] | null = null
|
||||
let tcfListenerId: number | undefined
|
||||
let gppListenerId: number | undefined
|
||||
let listenerInstalled = false
|
||||
let gppListenerInstalled = false
|
||||
let gppListenerAttempts = 0
|
||||
let gppInstallTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
let managingPreferences = false
|
||||
let consentComplete = false
|
||||
let consentVariant: ConsentVariant | null = null
|
||||
let uspConsentCommitPending = false
|
||||
let uspSuccessModalDismissed = false
|
||||
let uspCommitTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
let consentContainerObserver: MutationObserver | undefined
|
||||
const consentContainerContains = new Map<HTMLElement, HTMLElement['contains']>()
|
||||
|
||||
@@ -56,31 +79,132 @@ function getTcfApi(): TcfApi | undefined {
|
||||
return (window as typeof window & { __tcfapi?: TcfApi }).__tcfapi
|
||||
}
|
||||
|
||||
function getGppApi(): GppApi | undefined {
|
||||
return (window as typeof window & { __gpp?: GppApi }).__gpp
|
||||
}
|
||||
|
||||
function setConsentUiHidden(hidden: boolean) {
|
||||
document.documentElement.classList.toggle(CMP_HIDDEN_CLASS, hidden)
|
||||
patchConsentFocusTrapContainer()
|
||||
}
|
||||
|
||||
function patchConsentFocusTrapContainer() {
|
||||
const container = document.querySelector<HTMLElement>('#qc-cmp2-ui')
|
||||
if (!container || consentContainerContains.has(container)) return
|
||||
const containers = document.querySelectorAll<HTMLElement>('#qc-cmp2-ui, #qc-cmp2-usp')
|
||||
|
||||
const originalContains = container.contains
|
||||
// InMobi's focus trap otherwise cancels clicks outside its hidden container.
|
||||
container.contains = (node: Node | null) =>
|
||||
document.documentElement.classList.contains(CMP_HIDDEN_CLASS) ||
|
||||
originalContains.call(container, node)
|
||||
consentContainerContains.set(container, originalContains)
|
||||
for (const container of containers) {
|
||||
if (consentContainerContains.has(container)) continue
|
||||
|
||||
const originalContains = container.contains
|
||||
// InMobi's focus trap otherwise cancels clicks outside its hidden container.
|
||||
container.contains = (node: Node | null) =>
|
||||
document.documentElement.classList.contains(CMP_HIDDEN_CLASS) ||
|
||||
originalContains.call(container, node)
|
||||
consentContainerContains.set(container, originalContains)
|
||||
}
|
||||
}
|
||||
|
||||
function detectConsentVariant(): ConsentVariant | null {
|
||||
let variant: ConsentVariant | null = null
|
||||
|
||||
if (document.getElementById('qc-cmp2-usp')) {
|
||||
variant = 'usp'
|
||||
} else if (document.getElementById('qc-cmp2-ui')) {
|
||||
variant = 'tcf'
|
||||
}
|
||||
|
||||
if (variant) consentVariant = variant
|
||||
return variant
|
||||
}
|
||||
|
||||
function getConsentContainers(): ParentNode[] {
|
||||
const containers = Array.from(
|
||||
document.querySelectorAll<HTMLElement>('#qc-cmp2-container, #qc-cmp2-main, #qc-cmp2-ui'),
|
||||
document.querySelectorAll<HTMLElement>(
|
||||
'#qc-cmp2-container, #qc-cmp2-main, #qc-cmp2-ui, #qc-cmp2-usp',
|
||||
),
|
||||
)
|
||||
|
||||
return containers.length > 0 ? containers : [document]
|
||||
}
|
||||
|
||||
function getUspConsentControls(): UspControls | null {
|
||||
const dialog = document.getElementById('qc-cmp2-usp')
|
||||
if (!dialog) return null
|
||||
|
||||
const toggles = Array.from(
|
||||
dialog.querySelectorAll<HTMLButtonElement>(
|
||||
'.qc-usp-container button.qc-cmp2-toggle[role="switch"]',
|
||||
),
|
||||
)
|
||||
const confirmButton = dialog.querySelector<HTMLButtonElement>(
|
||||
'.qc-usp-ui-form-content button[mode="primary"]',
|
||||
)
|
||||
|
||||
if (
|
||||
toggles.length === 0 ||
|
||||
!confirmButton ||
|
||||
confirmButton.disabled ||
|
||||
toggles.some(
|
||||
(toggle) =>
|
||||
toggle.disabled || !['true', 'false'].includes(toggle.getAttribute('aria-checked') ?? ''),
|
||||
)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return { toggles, confirmButton }
|
||||
}
|
||||
|
||||
function waitForUspToggleState(
|
||||
index: number,
|
||||
checked: boolean,
|
||||
expectedCount: number,
|
||||
deadline: number,
|
||||
): Promise<UspControls | null> {
|
||||
return new Promise((resolve) => {
|
||||
function checkState() {
|
||||
const controls = getUspConsentControls()
|
||||
if (
|
||||
controls &&
|
||||
controls.toggles.length === expectedCount &&
|
||||
controls.toggles[index]?.getAttribute('aria-checked') === String(checked)
|
||||
) {
|
||||
resolve(controls)
|
||||
} else if (Date.now() >= deadline) {
|
||||
resolve(null)
|
||||
} else {
|
||||
setTimeout(checkState, 50)
|
||||
}
|
||||
}
|
||||
|
||||
checkState()
|
||||
})
|
||||
}
|
||||
|
||||
async function setUspToggleStates(
|
||||
checked: boolean,
|
||||
controls: UspControls,
|
||||
timeoutMs: number,
|
||||
): Promise<UspControls | null> {
|
||||
const expectedCount = controls.toggles.length
|
||||
const deadline = Date.now() + timeoutMs
|
||||
|
||||
for (let index = 0; index < expectedCount; index += 1) {
|
||||
const currentControls = getUspConsentControls()
|
||||
if (!currentControls || currentControls.toggles.length !== expectedCount) return null
|
||||
controls = currentControls
|
||||
|
||||
const toggle = controls.toggles[index]
|
||||
if (toggle.getAttribute('aria-checked') !== String(checked)) {
|
||||
toggle.click()
|
||||
const settledControls = await waitForUspToggleState(index, checked, expectedCount, deadline)
|
||||
if (!settledControls) return null
|
||||
controls = settledControls
|
||||
}
|
||||
}
|
||||
|
||||
return controls
|
||||
}
|
||||
|
||||
function matchesButtonText(button: HTMLButtonElement, terms: string[]): boolean {
|
||||
const text = [button.textContent, button.getAttribute('aria-label')]
|
||||
.filter(Boolean)
|
||||
@@ -173,9 +297,29 @@ function clickConsentButtonWhenReady(action: ConsentAction, timeoutMs: number):
|
||||
}
|
||||
|
||||
async function performConsentAction(action: ConsentAction) {
|
||||
const variant = detectConsentVariant()
|
||||
|
||||
if (action === 'manage') {
|
||||
managingPreferences = true
|
||||
setConsentUiHidden(false)
|
||||
if (variant === 'usp') return
|
||||
}
|
||||
|
||||
if (variant === 'usp' && action !== 'manage') {
|
||||
const controls = getUspConsentControls()
|
||||
const settledControls = controls
|
||||
? await setUspToggleStates(action === 'reject', controls, 2000)
|
||||
: null
|
||||
|
||||
if (settledControls) {
|
||||
beginUspConsentCommit()
|
||||
settledControls.confirmButton.click()
|
||||
return
|
||||
}
|
||||
|
||||
managingPreferences = false
|
||||
setConsentUiHidden(false)
|
||||
return
|
||||
}
|
||||
|
||||
const clicked = await clickConsentButtonWhenReady(action, action === 'manage' ? 2500 : 1000)
|
||||
@@ -194,6 +338,8 @@ async function performConsentAction(action: ConsentAction) {
|
||||
}
|
||||
|
||||
function showConsentNotification() {
|
||||
if (consentComplete) return
|
||||
|
||||
setConsentUiHidden(true)
|
||||
|
||||
if (
|
||||
@@ -239,6 +385,10 @@ function showConsentNotification() {
|
||||
|
||||
function finishConsent() {
|
||||
managingPreferences = false
|
||||
consentComplete = true
|
||||
uspConsentCommitPending = false
|
||||
clearTimeout(uspCommitTimeout)
|
||||
uspCommitTimeout = undefined
|
||||
setConsentUiHidden(false)
|
||||
|
||||
if (notificationId !== null) {
|
||||
@@ -247,8 +397,67 @@ function finishConsent() {
|
||||
}
|
||||
}
|
||||
|
||||
function beginUspConsentCommit() {
|
||||
if (!document.getElementById('qc-cmp2-usp')) return
|
||||
|
||||
consentVariant = 'usp'
|
||||
uspConsentCommitPending = true
|
||||
uspSuccessModalDismissed = false
|
||||
clearTimeout(uspCommitTimeout)
|
||||
|
||||
const deadline = Date.now() + 2500
|
||||
function checkForDialogClosure() {
|
||||
if (!uspConsentCommitPending) return
|
||||
|
||||
if (!document.getElementById('qc-cmp2-usp')) {
|
||||
finishConsent()
|
||||
} else if (Date.now() >= deadline) {
|
||||
uspConsentCommitPending = false
|
||||
uspCommitTimeout = undefined
|
||||
} else {
|
||||
uspCommitTimeout = setTimeout(checkForDialogClosure, 50)
|
||||
}
|
||||
}
|
||||
|
||||
uspCommitTimeout = setTimeout(checkForDialogClosure, 50)
|
||||
}
|
||||
|
||||
function dismissUspSuccessModal() {
|
||||
if ((!uspConsentCommitPending && !consentComplete) || uspSuccessModalDismissed) return
|
||||
|
||||
const closeButton = document.querySelector<HTMLElement>(
|
||||
'#qc-cmp2-usp [aria-label="Close success modal"]',
|
||||
)
|
||||
if (!closeButton || (closeButton instanceof HTMLButtonElement && closeButton.disabled)) return
|
||||
|
||||
uspSuccessModalDismissed = true
|
||||
closeButton.click()
|
||||
}
|
||||
|
||||
function syncConsentUi() {
|
||||
patchConsentFocusTrapContainer()
|
||||
installGppConsentListener()
|
||||
const variant = detectConsentVariant()
|
||||
dismissUspSuccessModal()
|
||||
|
||||
if (
|
||||
variant === 'usp' &&
|
||||
!consentComplete &&
|
||||
!uspConsentCommitPending &&
|
||||
!managingPreferences &&
|
||||
getUspConsentControls()
|
||||
) {
|
||||
showConsentNotification()
|
||||
}
|
||||
|
||||
if (uspConsentCommitPending && !document.getElementById('qc-cmp2-usp')) {
|
||||
finishConsent()
|
||||
}
|
||||
}
|
||||
|
||||
function handleTcfConsentEvent(data: TcfData, success: boolean) {
|
||||
if (!success) return
|
||||
detectConsentVariant()
|
||||
|
||||
if (data.listenerId !== undefined) {
|
||||
tcfListenerId = data.listenerId
|
||||
@@ -258,11 +467,47 @@ function handleTcfConsentEvent(data: TcfData, success: boolean) {
|
||||
if (!managingPreferences) {
|
||||
showConsentNotification()
|
||||
}
|
||||
} else if (data.eventStatus === 'useractioncomplete') {
|
||||
} else if (data.eventStatus === 'useractioncomplete' && consentVariant === 'tcf') {
|
||||
finishConsent()
|
||||
}
|
||||
}
|
||||
|
||||
function handleGppConsentEvent(data: GppData, success: boolean) {
|
||||
if (data.listenerId !== undefined) {
|
||||
gppListenerId = data.listenerId
|
||||
}
|
||||
|
||||
if (
|
||||
success &&
|
||||
data.eventName === 'sectionChange' &&
|
||||
consentVariant === 'usp' &&
|
||||
uspConsentCommitPending
|
||||
) {
|
||||
finishConsent()
|
||||
}
|
||||
}
|
||||
|
||||
function installGppConsentListener() {
|
||||
if (gppListenerInstalled) return
|
||||
|
||||
const gppApi = getGppApi()
|
||||
if (!gppApi) {
|
||||
if (gppListenerAttempts < 60 && gppInstallTimeout === undefined) {
|
||||
gppListenerAttempts += 1
|
||||
gppInstallTimeout = setTimeout(() => {
|
||||
gppInstallTimeout = undefined
|
||||
installGppConsentListener()
|
||||
}, 500)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
gppListenerInstalled = true
|
||||
clearTimeout(gppInstallTimeout)
|
||||
gppInstallTimeout = undefined
|
||||
gppApi('addEventListener', handleGppConsentEvent)
|
||||
}
|
||||
|
||||
function installTcfConsentListener() {
|
||||
if (listenerInstalled) return
|
||||
|
||||
@@ -273,31 +518,56 @@ function installTcfConsentListener() {
|
||||
tcfApi('addEventListener', 2, handleTcfConsentEvent)
|
||||
}
|
||||
|
||||
function handleDocumentClick(event: MouseEvent) {
|
||||
if (!managingPreferences || !(event.target instanceof Element)) return
|
||||
if (!event.target.closest('.qc-cmp2-close-icon')) return
|
||||
|
||||
function restoreConsentNotification() {
|
||||
setTimeout(() => {
|
||||
if (notificationId === null) return
|
||||
|
||||
managingPreferences = false
|
||||
setConsentUiHidden(true)
|
||||
showConsentNotification()
|
||||
})
|
||||
}
|
||||
|
||||
function handleDocumentClick(event: MouseEvent) {
|
||||
if (!(event.target instanceof Element)) return
|
||||
|
||||
if (event.target.closest('#qc-cmp2-usp .qc-usp-ui-form-content button[mode="primary"]')) {
|
||||
beginUspConsentCommit()
|
||||
return
|
||||
}
|
||||
|
||||
if (!managingPreferences) return
|
||||
if (!event.target.closest('.qc-cmp2-close-icon, .qc-usp-close-icon')) return
|
||||
|
||||
restoreConsentNotification()
|
||||
}
|
||||
|
||||
function handleDocumentKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && managingPreferences) {
|
||||
restoreConsentNotification()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
consentContainerObserver = new MutationObserver(patchConsentFocusTrapContainer)
|
||||
consentContainerObserver.observe(document.body, { childList: true, subtree: true })
|
||||
patchConsentFocusTrapContainer()
|
||||
consentContainerObserver = new MutationObserver(syncConsentUi)
|
||||
consentContainerObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['aria-label', 'disabled'],
|
||||
})
|
||||
syncConsentUi()
|
||||
installTcfConsentListener()
|
||||
installGppConsentListener()
|
||||
window.addEventListener('modrinth-cmp-ready', installTcfConsentListener)
|
||||
document.addEventListener('click', handleDocumentClick, true)
|
||||
document.addEventListener('keydown', handleDocumentKeydown, true)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
consentContainerObserver?.disconnect()
|
||||
clearTimeout(uspCommitTimeout)
|
||||
clearTimeout(gppInstallTimeout)
|
||||
window.removeEventListener('modrinth-cmp-ready', installTcfConsentListener)
|
||||
document.removeEventListener('click', handleDocumentClick, true)
|
||||
document.removeEventListener('keydown', handleDocumentKeydown, true)
|
||||
setConsentUiHidden(false)
|
||||
for (const [container, originalContains] of consentContainerContains) {
|
||||
container.contains = originalContains
|
||||
@@ -309,6 +579,11 @@ onBeforeUnmount(() => {
|
||||
tcfApi('removeEventListener', 2, () => {}, tcfListenerId)
|
||||
}
|
||||
|
||||
const gppApi = getGppApi()
|
||||
if (gppApi && gppListenerId !== undefined) {
|
||||
gppApi('removeEventListener', () => {}, gppListenerId)
|
||||
}
|
||||
|
||||
if (notificationId !== null) {
|
||||
notificationManager.removeNotification(notificationId)
|
||||
}
|
||||
@@ -319,7 +594,8 @@ onBeforeUnmount(() => {
|
||||
html.modrinth-cmp-summary-hidden .qc-cmp2-container,
|
||||
html.modrinth-cmp-summary-hidden #qc-cmp2-container,
|
||||
html.modrinth-cmp-summary-hidden #qc-cmp2-main,
|
||||
html.modrinth-cmp-summary-hidden #qc-cmp2-ui {
|
||||
html.modrinth-cmp-summary-hidden #qc-cmp2-ui,
|
||||
html.modrinth-cmp-summary-hidden #qc-cmp2-usp {
|
||||
display: none !important;
|
||||
z-index: -1 !important;
|
||||
pointer-events: none !important;
|
||||
|
||||
Reference in New Issue
Block a user