xref: /expo/docs/ui/components/RouteUrl/index.tsx (revision 6eefacaa)
1import { Link04Icon } from '@expo/styleguide-icons';
2import React, { PropsWithChildren } from 'react';
3
4import { SnippetHeader } from '../Snippet/SnippetHeader';
5import { RuntimePopup } from './RuntimePopup';
6
7type ProtocolType = 'expo-go' | 'custom' | 'web';
8
9const mapping: { name: string; id: ProtocolType }[] = [
10  { name: 'Expo Go', id: 'expo-go' },
11  { name: 'Custom', id: 'custom' },
12  { name: 'Web', id: 'web' },
13];
14
15const SharedContext = React.createContext<{
16  type: ProtocolType;
17  setType: (type: ProtocolType) => void;
18} | null>(null);
19
20/**
21 * Wraps a group of route urls and shares the preference.
22 */
23export function RouteUrlGroup({ children }: { children: React.ReactNode }) {
24  const [type, setType] = React.useState<ProtocolType>('custom');
25  return <SharedContext.Provider value={{ type, setType }}>{children}</SharedContext.Provider>;
26}
27
28function getProtocol(type: ProtocolType) {
29  return { 'expo-go': 'exp://127.0.0.1:8081/--/', web: 'acme.dev/', custom: 'acme://' }[type];
30}
31
32export const RouteUrl = (props: PropsWithChildren) => {
33  const context = React.useContext(SharedContext);
34  const [type, setType] = React.useState<ProtocolType>('custom');
35
36  if (context) {
37    return <RouteUrlInner {...props} {...context} />;
38  }
39
40  return <RouteUrlInner {...props} type={type} setType={setType} />;
41};
42
43const RouteUrlInner = ({
44  children,
45  type,
46  setType,
47}: PropsWithChildren<{ type: ProtocolType; setType: (type: ProtocolType) => void }>) => {
48  const protocol = getProtocol(type);
49
50  const childrenString = React.useMemo(
51    () =>
52      React.Children.map(children, child => {
53        // return strings
54        if (typeof child === 'string') {
55          return child;
56        }
57        return null;
58      })
59        ?.filter(Boolean)
60        .join('')
61        .trim() || '/',
62    [children]
63  );
64
65  const [parsedProtocol, inputUrl] = React.useMemo<[string, string]>(() => {
66    if (type === 'custom') {
67      if (childrenString === '/') {
68        // remove last slash
69        return [protocol.replace(/\/$/, ''), childrenString];
70      } else {
71        // ensure no starting slashes
72        return [protocol, childrenString.replace(/^\/+/, '')];
73      }
74    }
75    // remove last slash
76    return [protocol.replace(/\/$/, ''), childrenString];
77  }, [type, childrenString, protocol]);
78
79  return (
80    <SnippetHeader
81      float
82      title={
83        <span className="select-all">
84          <span className="text-icon-secondary ">{parsedProtocol}</span>
85          {inputUrl}
86        </span>
87      }
88      Icon={Link04Icon}>
89      <RuntimePopup items={mapping} selected={type} onSelect={value => setType(value)} />
90    </SnippetHeader>
91  );
92};
93