|
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 {
|
@@ -187,49 +242,15 @@ export function OnboardingOptionButtons({
|
187 | 242 | });
|
188 | 243 | });
|
189 | 244 | }
|
| 245 | + |
| 246 | + // sync local state to global |
190 | 247 | useEffect(() => {
|
191 |
| - options.forEach(option => { |
192 |
| - if (option.disabled) { |
193 |
| - return; |
194 |
| - } |
195 |
| - const targetElements = document.querySelectorAll<HTMLDivElement>( |
196 |
| - `[data-onboarding-option="${option.id}"]` |
197 |
| - ); |
198 |
| - targetElements.forEach(el => { |
199 |
| - const hiddenForThisOption = el.dataset.hideForThisOption === 'true'; |
200 |
| - if (hiddenForThisOption) { |
201 |
| - el.classList.toggle('hidden', option.checked); |
202 |
| - } else { |
203 |
| - el.classList.toggle('hidden', !option.checked); |
204 |
| - } |
205 |
| - // only animate things when user has interacted with the options |
206 |
| - if (touchedOptions) { |
207 |
| - if (el.classList.contains('code-line')) { |
208 |
| - el.classList.toggle('animate-line', option.checked); |
209 |
| - } |
210 |
| - // animate content, account for inverted logic for hiding |
211 |
| - else { |
212 |
| - el.classList.toggle( |
213 |
| - 'animate-content', |
214 |
| - hiddenForThisOption ? !option.checked : option.checked |
215 |
| - ); |
216 |
| - } |
217 |
| - } |
218 |
| - }); |
219 |
| - if (option.checked && optionDetails[option.id].deps?.length) { |
220 |
| - const dependenciesSelecor = optionDetails[option.id].deps!.map( |
221 |
| - dep => `[data-onboarding-option="${dep}"]` |
222 |
| - ); |
223 |
| - const dependencies = document.querySelectorAll<HTMLDivElement>( |
224 |
| - dependenciesSelecor.join(', ') |
225 |
| - ); |
| 248 | + codeContext?.updateOnboardingOptions(options); |
| 249 | + }, [options, codeContext]); |
226 | 250 |
|
227 |
| - dependencies.forEach(dep => { |
228 |
| - dep.classList.remove('hidden'); |
229 |
| - }); |
230 |
| - } |
231 |
| - }); |
232 |
| - }, [options, touchedOptions]); |
| 251 | + useEffect(() => { |
| 252 | + updateElementsVisibilityForOptions(options, touchedOptions); |
| 253 | + }, [options, touchOptions, touchedOptions]); |
233 | 254 |
|
234 | 255 | return (
|
235 | 256 | <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