|
1 | 1 | 'use client'; |
2 | 2 |
|
3 | | -import {ReactNode, useEffect, useReducer, useState} from 'react'; |
| 3 | +import {ReactNode, useContext, useEffect, useReducer, useState} from 'react'; |
4 | 4 | import {QuestionMarkCircledIcon} from '@radix-ui/react-icons'; |
5 | 5 | import * as Tooltip from '@radix-ui/react-tooltip'; |
6 | 6 | import {Button, Checkbox, Theme} from '@radix-ui/themes'; |
7 | 7 |
|
8 | 8 | import styles from './styles.module.scss'; |
9 | 9 |
|
| 10 | +import {CodeContext} from '../codeContext'; |
| 11 | + |
10 | 12 | const optionDetails: Record< |
11 | 13 | OptionId, |
12 | 14 | { |
@@ -74,7 +76,7 @@ const OPTION_IDS = [ |
74 | 76 |
|
75 | 77 | type OptionId = (typeof OPTION_IDS)[number]; |
76 | 78 |
|
77 | | -type OnboardingOptionType = { |
| 79 | +export type OnboardingOptionType = { |
78 | 80 | /** |
79 | 81 | * Unique identifier for the option, will control the visibility |
80 | 82 | * of `<OnboardingOption optionId="this_id"` /> somewhere on the page |
@@ -119,12 +121,65 @@ export function OnboardingOption({ |
119 | 121 | ); |
120 | 122 | } |
121 | 123 |
|
| 124 | +/** |
| 125 | + * Updates DOM elements' visibility based on selected onboarding options |
| 126 | + */ |
| 127 | +export function updateElementsVisibilityForOptions( |
| 128 | + options: OnboardingOptionType[], |
| 129 | + touchedOptions: boolean |
| 130 | +) { |
| 131 | + options.forEach(option => { |
| 132 | + if (option.disabled) { |
| 133 | + return; |
| 134 | + } |
| 135 | + const targetElements = document.querySelectorAll<HTMLDivElement>( |
| 136 | + `[data-onboarding-option="${option.id}"]` |
| 137 | + ); |
| 138 | + |
| 139 | + targetElements.forEach(el => { |
| 140 | + const hiddenForThisOption = el.dataset.hideForThisOption === 'true'; |
| 141 | + if (hiddenForThisOption) { |
| 142 | + el.classList.toggle('hidden', option.checked); |
| 143 | + } else { |
| 144 | + el.classList.toggle('hidden', !option.checked); |
| 145 | + } |
| 146 | + // only animate things when user has interacted with the options |
| 147 | + if (touchedOptions) { |
| 148 | + if (el.classList.contains('code-line')) { |
| 149 | + el.classList.toggle('animate-line', option.checked); |
| 150 | + } |
| 151 | + // animate content, account for inverted logic for hiding |
| 152 | + else { |
| 153 | + el.classList.toggle( |
| 154 | + 'animate-content', |
| 155 | + hiddenForThisOption ? !option.checked : option.checked |
| 156 | + ); |
| 157 | + } |
| 158 | + } |
| 159 | + }); |
| 160 | + if (option.checked && optionDetails[option.id].deps?.length) { |
| 161 | + const dependenciesSelector = optionDetails[option.id].deps!.map( |
| 162 | + dep => `[data-onboarding-option="${dep}"]` |
| 163 | + ); |
| 164 | + const dependencies = document.querySelectorAll<HTMLDivElement>( |
| 165 | + dependenciesSelector.join(', ') |
| 166 | + ); |
| 167 | + |
| 168 | + dependencies.forEach(dep => { |
| 169 | + dep.classList.remove('hidden'); |
| 170 | + }); |
| 171 | + } |
| 172 | + }); |
| 173 | +} |
| 174 | + |
122 | 175 | export function OnboardingOptionButtons({ |
123 | 176 | options: initialOptions, |
124 | 177 | }: { |
125 | 178 | // convenience to allow passing option ids as strings when no additional config is required |
126 | 179 | options: (OnboardingOptionType | OptionId)[]; |
127 | 180 | }) { |
| 181 | + const codeContext = useContext(CodeContext); |
| 182 | + |
128 | 183 | const normalizedOptions = initialOptions.map(option => { |
129 | 184 | if (typeof option === 'string') { |
130 | 185 | return {id: option, disabled: option === 'error-monitoring'}; |
@@ -182,49 +237,15 @@ export function OnboardingOptionButtons({ |
182 | 237 | }); |
183 | 238 | }); |
184 | 239 | } |
| 240 | + |
| 241 | + // sync local state to global |
185 | 242 | useEffect(() => { |
186 | | - options.forEach(option => { |
187 | | - if (option.disabled) { |
188 | | - return; |
189 | | - } |
190 | | - const targetElements = document.querySelectorAll<HTMLDivElement>( |
191 | | - `[data-onboarding-option="${option.id}"]` |
192 | | - ); |
193 | | - targetElements.forEach(el => { |
194 | | - const hiddenForThisOption = el.dataset.hideForThisOption === 'true'; |
195 | | - if (hiddenForThisOption) { |
196 | | - el.classList.toggle('hidden', option.checked); |
197 | | - } else { |
198 | | - el.classList.toggle('hidden', !option.checked); |
199 | | - } |
200 | | - // only animate things when user has interacted with the options |
201 | | - if (touchedOptions) { |
202 | | - if (el.classList.contains('code-line')) { |
203 | | - el.classList.toggle('animate-line', option.checked); |
204 | | - } |
205 | | - // animate content, account for inverted logic for hiding |
206 | | - else { |
207 | | - el.classList.toggle( |
208 | | - 'animate-content', |
209 | | - hiddenForThisOption ? !option.checked : option.checked |
210 | | - ); |
211 | | - } |
212 | | - } |
213 | | - }); |
214 | | - if (option.checked && optionDetails[option.id].deps?.length) { |
215 | | - const dependenciesSelecor = optionDetails[option.id].deps!.map( |
216 | | - dep => `[data-onboarding-option="${dep}"]` |
217 | | - ); |
218 | | - const dependencies = document.querySelectorAll<HTMLDivElement>( |
219 | | - dependenciesSelecor.join(', ') |
220 | | - ); |
| 243 | + codeContext?.updateOnboardingOptions(options); |
| 244 | + }, [options, codeContext]); |
221 | 245 |
|
222 | | - dependencies.forEach(dep => { |
223 | | - dep.classList.remove('hidden'); |
224 | | - }); |
225 | | - } |
226 | | - }); |
227 | | - }, [options, touchedOptions]); |
| 246 | + useEffect(() => { |
| 247 | + updateElementsVisibilityForOptions(options, touchedOptions); |
| 248 | + }, [options, touchOptions, touchedOptions]); |
228 | 249 |
|
229 | 250 | return ( |
230 | 251 | <div className="onboarding-options flex flex-wrap gap-3 py-2 bg-[var(--white)] dark:bg-[var(--gray-1)] sticky top-[80px] z-[4] shadow-[var(--shadow-6)] transition"> |
|
0 commit comments