aboutsummaryrefslogtreecommitdiffstats
path: root/packages/react-shared/src/components/link.tsx
blob: 2fb19ac11379f2383ab10be4e5383d165674a422 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import * as _ from 'lodash';
import * as React from 'react';
import { NavLink as ReactRounterLink } from 'react-router-dom';
import { Link as ScrollLink } from 'react-scroll';
import * as validUrl from 'valid-url';

import { LinkType } from '../types';
import { constants } from '../utils/constants';

export interface BaseLinkProps {
    to: string;
    shouldOpenInNewTab?: boolean;
    className?: string;
    onMouseOver?: (event: React.MouseEvent<HTMLElement>) => void;
    onMouseLeave?: (event: React.MouseEvent<HTMLElement>) => void;
    onMouseEnter?: (event: React.MouseEvent<HTMLElement>) => void;
    textDecoration?: string;
    fontColor?: string;
}

export interface ScrollLinkProps extends BaseLinkProps {
    onActivityChanged?: (isActive: boolean) => void;
}

export interface ReactLinkProps extends BaseLinkProps {
    activeStyle?: React.CSSProperties;
}

export type LinkProps = ReactLinkProps & ScrollLinkProps;

export interface LinkState {}

/**
 * A generic link component which let's the developer render internal, external and scroll-to-hash links, and
 * their associated behaviors with a single link component. Many times we want a menu including a combination of
 * internal, external and scroll links and the abstraction of the differences of rendering each types of link
 * makes it much easier to do so.
 */
export class Link extends React.Component<LinkProps, LinkState> {
    public static defaultProps: Partial<LinkProps> = {
        shouldOpenInNewTab: false,
        className: '',
        onMouseOver: _.noop.bind(_),
        onMouseLeave: _.noop.bind(_),
        onMouseEnter: _.noop.bind(_),
        textDecoration: 'none',
        fontColor: 'inherit',
    };
    private _outerReactScrollSpan: HTMLSpanElement | null;
    constructor(props: LinkProps) {
        super(props);
        this._outerReactScrollSpan = null;
    }
    public render(): React.ReactNode {
        let type: LinkType;
        const isReactRoute = _.startsWith(this.props.to, '/');
        const isExternal = validUrl.isWebUri(this.props.to) || _.startsWith(this.props.to, 'mailto:');
        if (isReactRoute) {
            type = LinkType.ReactRoute;
        } else if (isExternal) {
            type = LinkType.External;
        } else {
            type = LinkType.ReactScroll;
        }

        if (type === LinkType.ReactScroll && this.props.shouldOpenInNewTab) {
            throw new Error(`Cannot open LinkType.ReactScroll links in new tab. link.to: ${this.props.to}`);
        }

        const styleWithDefault = {
            textDecoration: this.props.textDecoration,
            cursor: 'pointer',
            color: this.props.fontColor,
        };

        switch (type) {
            case LinkType.External:
                return (
                    <a
                        target={this.props.shouldOpenInNewTab ? '_blank' : ''}
                        className={this.props.className}
                        style={styleWithDefault}
                        href={this.props.to}
                        onMouseOver={this.props.onMouseOver}
                        onMouseEnter={this.props.onMouseEnter}
                        onMouseLeave={this.props.onMouseLeave}
                    >
                        {this.props.children}
                    </a>
                );
            case LinkType.ReactRoute:
                return (
                    <ReactRounterLink
                        to={this.props.to}
                        className={this.props.className}
                        style={styleWithDefault}
                        target={this.props.shouldOpenInNewTab ? '_blank' : ''}
                        onMouseOver={this.props.onMouseOver}
                        onMouseEnter={this.props.onMouseEnter}
                        onMouseLeave={this.props.onMouseLeave}
                        activeStyle={this.props.activeStyle}
                    >
                        {this.props.children}
                    </ReactRounterLink>
                );
            case LinkType.ReactScroll:
                return (
                    <span
                        ref={input => (this._outerReactScrollSpan = input)}
                        onMouseOver={this.props.onMouseOver}
                        onMouseEnter={this.props.onMouseEnter}
                        onMouseLeave={this.props.onMouseLeave}
                    >
                        <ScrollLink
                            to={this.props.to}
                            offset={0}
                            spy={true}
                            hashSpy={true}
                            duration={constants.DOCS_SCROLL_DURATION_MS}
                            containerId={constants.SCROLL_CONTAINER_ID}
                            className={this.props.className}
                            style={styleWithDefault}
                            onSetActive={this._onActivityChanged.bind(this, true)}
                            onSetInactive={this._onActivityChanged.bind(this, false)}
                        >
                            <span onClick={this._onClickPropagateClickEventAroundScrollLink.bind(this)}>
                                {this.props.children}
                            </span>
                        </ScrollLink>
                    </span>
                );
            default:
                throw new Error(`Unrecognized LinkType: ${type}`);
        }
    }
    private _onActivityChanged(isActive: boolean): void {
        if (this.props.onActivityChanged) {
            this.props.onActivityChanged(isActive);
        }
    }
    // HACK(fabio): For some reason, the react-scroll link decided to stop the propagation of click events.
    // We do however rely on these events being propagated in certain scenarios (e.g when the link
    // is within a dropdown we want to close upon being clicked). Because of this, we register the
    // click event of an inner span, and pass it around the react-scroll link to an outer span.
    private _onClickPropagateClickEventAroundScrollLink(): void {
        if (!_.isNull(this._outerReactScrollSpan)) {
            this._outerReactScrollSpan.click();
        }
    }
}