ariaLabel: lf()

in webapp/src/tutorial.tsx [345:659]


                    ariaLabel: lf("Launch Immersive Reader"),
                    title: lf("Launch Immersive Reader")
                })
            }
            actions.push({
                label: hideIteration && flyoutOnly ? lf("Start") : lf("Ok"),
                onclick: onClick,
                icon: 'check',
                className: 'green'
            });

            const classes = this.props.parent.createModalClasses("hintdialog");

            return <sui.Modal isOpen={visible} className={classes}
                closeIcon={false} header={tutorialName} buttons={actions}
                onClose={onClick} dimmer={true} longer={true}
                closeOnDimmerClick closeOnDocumentClick closeOnEscape>
                <md.MarkedContent markdown={fullText} parent={this.props.parent} />
            </sui.Modal>
        }
    }
}

interface TutorialCardState {
    showHint?: boolean;
    showSeeMore?: boolean;
    showTutorialValidationMessage?: boolean;
}

interface TutorialCardProps extends ISettingsProps {
    pokeUser?: boolean;
}

export class TutorialCard extends data.Component<TutorialCardProps, TutorialCardState> {
    private prevStep: number;
    private cardHeight: number;
    private resizeDebouncer: () => void;

    public focusInitialized: boolean;

    constructor(props: ISettingsProps) {
        super(props);
        const options = this.props.parent.state.tutorialOptions;
        this.prevStep = options.tutorialStep;

        this.state = {
            showSeeMore: false,
            showHint: options.tutorialStepInfo[this.prevStep].showHint,
            showTutorialValidationMessage: false
        }

        this.toggleHint = this.toggleHint.bind(this);
        this.closeHint = this.closeHint.bind(this);
        this.hintOnClick = this.hintOnClick.bind(this);
        this.closeLightbox = this.closeLightbox.bind(this);
        this.tutorialCardKeyDown = this.tutorialCardKeyDown.bind(this);
        this.okButtonKeyDown = this.okButtonKeyDown.bind(this);
        this.previousTutorialStep = this.previousTutorialStep.bind(this);
        this.nextTutorialStep = this.nextTutorialStep.bind(this);
        this.finishTutorial = this.finishTutorial.bind(this);
        this.toggleExpanded = this.toggleExpanded.bind(this);
        this.onMarkdownDidRender = this.onMarkdownDidRender.bind(this);
        this.handleResize = this.handleResize.bind(this);
        this.showTutorialValidationMessageOnClick = this.showTutorialValidationMessageOnClick.bind(this);
        this.closeTutorialValidationMessage = this.closeTutorialValidationMessage.bind(this);
        this.doubleClickedNextStep = this.doubleClickedNextStep.bind(this);
        this.validationTelemetry = this.validationTelemetry.bind(this);
    }

    previousTutorialStep() {
        this.showHint(false); // close hint on new tutorial step
        let options = this.props.parent.state.tutorialOptions;
        const currentStep = options.tutorialStep;
        const previousStep = currentStep - 1;

        options.tutorialStep = previousStep;

        pxt.tickEvent(`tutorial.previous`, { tutorial: options.tutorial, step: previousStep }, { interactiveConsent: true });
        this.props.parent.setTutorialStep(previousStep);
        this.setState({ showTutorialValidationMessage: false });
    }

    nextTutorialStep() {
        this.showHint(false); // close hint on new tutorial step
        let options = this.props.parent.state.tutorialOptions;
        const currentStep = options.tutorialStep;
        const nextStep = currentStep + 1;

        options.tutorialStep = nextStep;

        pxt.tickEvent(`tutorial.next`, { tutorial: options.tutorial, step: nextStep }, { interactiveConsent: true });
        this.props.parent.setTutorialStep(nextStep);

        const tutorialCodeValidationIsOn = options.metadata.tutorialCodeValidation;
        if (tutorialCodeValidationIsOn && this.state.showTutorialValidationMessage) { // disables tutorial validation pop-up if next buttion is clicked
            this.setState({ showTutorialValidationMessage: false });
        }
    }

    doubleClickedNextStep() {
        this.validationTelemetry('next');
        this.nextTutorialStep();
    }

    finishTutorial() {
        this.closeLightbox();
        this.removeHintOnClick();
        this.props.parent.completeTutorialAsync();
    }

    private closeLightboxOnEscape = (e: KeyboardEvent) => {
        const charCode = core.keyCodeFromEvent(e);
        if (charCode === 27) {
            this.closeLightbox();
        }
    }

    private closeLightbox() {
        sounds.tutorialNext();
        document.documentElement.removeEventListener("keydown", this.closeLightboxOnEscape);

        // Hide lightbox
        this.props.parent.hideLightbox();
    }

    UNSAFE_componentWillUpdate() {
        document.documentElement.addEventListener("keydown", this.closeLightboxOnEscape);
    }

    private tutorialCardKeyDown(e: KeyboardEvent) {
        const charCode = core.keyCodeFromEvent(e);
        if (charCode == core.TAB_KEY) {
            e.preventDefault();
            const tutorialOkRef = this.refs["tutorialok"] as sui.Button;
            const okButton = ReactDOM.findDOMNode(tutorialOkRef) as HTMLElement;
            okButton.focus();
        }
    }

    private okButtonKeyDown(e: KeyboardEvent) {
        const charCode = core.keyCodeFromEvent(e);
        if (charCode == core.TAB_KEY) {
            e.preventDefault();
            const tutorialCard = this.refs['tutorialmessage'] as HTMLElement;
            tutorialCard.focus();
        }
    }

    private lastStep = -1;
    componentDidUpdate(prevProps: ISettingsProps, prevState: TutorialCardState) {
        const options = this.props.parent.state.tutorialOptions;
        const tutorialCard = this.refs['tutorialmessage'] as HTMLElement;

        const step = this.props.parent.state.tutorialOptions.tutorialStep;
        if (step != this.lastStep) {
            const animationClasses = `fade ${step < this.lastStep ? "right" : "left"} in visible transition animating`;
            tutorialCard.style.animationDuration = '500ms';
            this.lastStep = step;
            pxsim.U.addClass(tutorialCard, animationClasses);
            pxt.Util.delay(500)
                .then(() => pxsim.U.removeClass(tutorialCard, animationClasses));
        }
        if (this.prevStep != step) {
            this.setShowSeeMore(options.autoexpandStep);
            this.prevStep = step;

            // on "new step", sync tutorial card state. used when exiting the modal, since that bypasses the react lifecycle
            this.setState({ showHint: options.tutorialStepInfo[step].showDialog || options.tutorialStepInfo[step].showHint })
        }
    }

    private handleResize() {
        const options = this.props.parent.state.tutorialOptions;
        this.setShowSeeMore(options.autoexpandStep);
    }

    componentDidMount() {
        this.setShowSeeMore(this.props.parent.state.tutorialOptions.autoexpandStep);
        this.resizeDebouncer = pxt.Util.debounce(this.handleResize, 500);
        window.addEventListener('resize', this.resizeDebouncer);
    }

    onMarkdownDidRender() {
        this.setShowSeeMore(this.props.parent.state.tutorialOptions.autoexpandStep);
    }

    componentWillUnmount() {
        // Clear the markdown cache when we unmount
        md.MarkedContent.clearBlockSnippetCache();
        this.lastStep = -1;

        // Clear any existing timers
        this.props.parent.stopPokeUserActivity();

        this.removeHintOnClick();
        window.removeEventListener('resize', this.resizeDebouncer);
    }

    private removeHintOnClick() {
        // cleanup hintOnClick
        document.removeEventListener('click', this.closeHint);
    }

    toggleExpanded(ev: React.MouseEvent<HTMLDivElement>) {
        ev.stopPropagation();
        ev.preventDefault();
        const options = this.props.parent.state.tutorialOptions;
        const { tutorialStepExpanded } = options;
        this.props.parent.setTutorialInstructionsExpanded(!tutorialStepExpanded);
        return false;
    }

    private hasHint() {
        const options = this.props.parent.state.tutorialOptions;
        const { tutorialReady, tutorialStepInfo, tutorialStep } = options;
        if (!tutorialReady) return false;
        return !!tutorialStepInfo[tutorialStep].hintContentMd
            || tutorialStepInfo[tutorialStep].showDialog;
    }

    private hintOnClick(evt?: any) {
        const options = this.props.parent.state.tutorialOptions;
        if (!options) {
            pxt.reportError("tutorial", "leaking hintonclick");
            return;
        }
        if (evt) evt.stopPropagation();
        const { tutorialStepInfo, tutorialStep } = options;
        const step = tutorialStepInfo[tutorialStep];
        const showDialog = tutorialStep < tutorialStepInfo.length - 1 && step && !!step.showDialog;

        this.props.parent.clearUserPoke();

        if (!showDialog) {
            this.toggleHint();
        }
    }

    private showTutorialValidationMessageOnClick(evt?: any) {
        this.setState({ showTutorialValidationMessage: true });
    }

    private expandedHintOnClick(evt?: any) {
        evt.stopPropagation();
    }

    private setShowSeeMore(autoexpand?: boolean) {
        // compare scrollHeight of inner text with height of card to determine showSeeMore
        const tutorialCard = this.refs['tutorialmessage'] as HTMLElement;
        let show = false;
        if (tutorialCard && tutorialCard.firstElementChild && tutorialCard.firstElementChild.firstElementChild) {
            show = tutorialCard.clientHeight <= tutorialCard.firstElementChild.firstElementChild.scrollHeight;
            if (show) {
                this.cardHeight = tutorialCard.firstElementChild.firstElementChild.scrollHeight;
                if (autoexpand) this.props.parent.setTutorialInstructionsExpanded(true);
            }
        }
        this.setState({ showSeeMore: show });
        this.props.parent.setEditorOffset();
    }

    getCardHeight() {
        return this.cardHeight;
    }

    getExpandedCardStyle(prop: string) {
        return { [prop]: `calc(${this.getCardHeight()}px + 2rem)` }
    }

    toggleHint(showFullText?: boolean) {
        this.showHint(!this.state.showHint, showFullText);
    }

    closeHint(evt?: any) {
        this.showHint(false);
    }

    showHint(visible: boolean, showFullText?: boolean) {
        this.removeHintOnClick();
        this.closeLightbox();
        if (!this.hasHint()) return;

        const th = this.refs["tutorialhint"] as TutorialHint;
        if (!th) return;
        const currentStep = this.props.parent.state.tutorialOptions.tutorialStep;

        if (!visible) {
            if (th.elementRef) th.elementRef.removeEventListener('click', this.expandedHintOnClick);
            this.setState({ showHint: false });
            this.props.parent.pokeUserActivity();
        } else {
            if (th.elementRef) th.elementRef.addEventListener('click', this.expandedHintOnClick);
            this.setState({ showHint: true });
            this.props.parent.stopPokeUserActivity();

            const options = this.props.parent.state.tutorialOptions;
            if (!options.tutorialStepInfo[options.tutorialStep].showDialog)
                document.addEventListener('click', this.closeHint); // add close listener if not modal
            pxt.tickEvent(`tutorial.showhint`, { tutorial: options.tutorial, step: options.tutorialStep });
            this.props.parent.setHintSeen(currentStep);
        }
        th.showHint(visible, showFullText);
        if (visible) {
            this.setState({ showTutorialValidationMessage: false });
        }
    }

    closeTutorialValidationMessage() {
        this.setState({ showTutorialValidationMessage: false });
    }

    isCodeValidated(rules: pxt.tutorial.TutorialRuleStatus[]) {
        if (rules != undefined) {
            for (let i = 0; i < rules.length; i++) {
                if (rules[i].ruleTurnOn && !rules[i].ruleStatus) {