on master help sign in
feat: implement dropdowns, tidy up hardware login
commitceff01556cb9d75cc7f764abcb65a037a7333321 authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateTue 2 Jul 2019, 20:03 (7 years ago) parentcd91628
14 files changed+346−141
src/components/Inputs.js+24−1 src/indigo-react/components/CheckboxInput.js−4 src/indigo-react/components/Flex.js+21−13 src/indigo-react/components/Grid.js+24−16 src/indigo-react/components/Input.js+3−19 src/indigo-react/components/SelectInput.js+147 src/indigo-react/lib/useForm.js+27−21 src/indigo-react/lib/useOnClickOutside.js+24 src/lib/validators.js+8−1 src/style/indigo-proposals.scss+4 src/views/Login/Ledger.js+38−36 src/views/Login/Mnemonic.js+1−1 src/views/Login/Ticket.js+1−1 src/views/Login/Trezor.js+24−28
src/components/Inputs.js+24−1
@@ -14,6 +14,7 @@ import {
1414 validateEmail,
1515 validateLength,
1616 validateHexString,
17+ validateOneOf,
1718} from 'lib/validators';
1819import { prependSig } from 'lib/transformers';
1920
@@ -187,7 +188,7 @@ export function usePointInput(rest) {
187188 accessory: lastValidPoint ? (
188189 <InputSigil
189190 patp={lastValidPoint}
190− size={68}
191+ size={44}
191192 margin={8}
192193 pass={pass}
193194 focused={focused}
@@ -209,6 +210,28 @@ export function useCheckboxInput({ initialValue, ...rest }) {
209210 );
210211}
211212
213+export function useSelectInput({ initialValue, options, ...rest }) {
214+ return useFirstOf(
215+ useForm([
216+ {
217+ type: 'select',
218+ validators: useMemo(
219+ () => [
220+ validateOneOf(
221+ options.map(option => option.value),
222+ validateNotEmpty
223+ ),
224+ ],
225+ [options]
226+ ),
227+ options,
228+ initialValue: initialValue || options[0].value,
229+ ...rest,
230+ },
231+ ])
232+ );
233+}
234+
212235const kEmailValidators = [validateEmail, validateNotEmpty];
213236export const buildEmailInputConfig = extra => ({
214237 type: 'email',
src/indigo-react/components/CheckboxInput.js−4
@@ -12,9 +12,6 @@ export default function CheckboxInput({
1212
1313 // callbacks
1414 onValue,
15− onPass,
16− onError,
17− onFocus,
1815 onEnter,
1916
2017 // state from hook
@@ -31,7 +28,6 @@ export default function CheckboxInput({
3128 initialValue,
3229 validators,
3330 transformers,
34− setValue,
3531
3632 // extra
3733 ...rest
src/indigo-react/components/Flex.js+21−13
@@ -1,21 +1,25 @@
11import React from 'react';
22import cn from 'classnames';
33
4−function Flex({
5− as: As = 'div',
6− row = false,
7− wrap = false,
8− col = false,
9− align,
10− justify,
11− className,
12− ...rest
13−}) {
4+const Flex = React.forwardRef(function Flex(
5+ {
6+ as: As = 'div',
7+ row = false,
8+ wrap = false,
9+ col = false,
10+ align,
11+ justify,
12+ className,
13+ ...rest
14+ },
15+ ref
16+) {
1417 if (row && col) {
1518 throw new Error('Only one of row or col must be true, not both.');
1619 }
1720 return (
1821 <As
22+ ref={ref}
1923 className={cn(
2024 'flex',
2125 {
@@ -32,16 +36,20 @@ function Flex({
3236 {...rest}
3337 />
3438 );
35−}
39+});
3640
3741// flex can be boolean {true} or integer flex
38−Flex.Item = function FlexItem({ as: As = 'div', flex, className, ...rest }) {
42+Flex.Item = React.forwardRef(function FlexItem(
43+ { as: As = 'div', flex, className, ...rest },
44+ ref
45+) {
3946 if (flex === true) {
4047 flex = 1;
4148 }
4249
4350 return (
4451 <As
52+ ref={ref}
4553 className={cn(
4654 {
4755 [`flex${flex}`]: flex,
@@ -51,6 +59,6 @@ Flex.Item = function FlexItem({ as: As = 'div', flex, className, ...rest }) {
5159 {...rest}
5260 />
5361 );
54−};
62+});
5563
5664export default Flex;
src/indigo-react/components/Grid.js+24−16
@@ -1,9 +1,13 @@
11import React from 'react';
22import cn from 'classnames';
33
4−function Grid({ as: As = 'div', gap = 0, align, justify, className, ...rest }) {
4+const Grid = React.forwardRef(function Grid(
5+ { as: As = 'div', gap = 0, align, justify, className, ...rest },
6+ ref
7+) {
58 return (
69 <As
10+ ref={ref}
711 className={cn(
812 'grid12',
913 gap && `gap${gap}`,
@@ -16,23 +20,27 @@ function Grid({ as: As = 'div', gap = 0, align, justify, className, ...rest }) {
1620 {...rest}
1721 />
1822 );
19−}
23+});
2024
21−Grid.Item = function GridItem({
22− as: As = 'div',
23− full = false,
24− half = 0,
25− third = 0,
26− fourth = 0,
27− rows = [],
28− cols = [],
29− justifySelf,
30− alignSelf,
31− className,
32− ...rest
33−}) {
25+Grid.Item = React.forwardRef(function GridItem(
26+ {
27+ as: As = 'div',
28+ full = false,
29+ half = 0,
30+ third = 0,
31+ fourth = 0,
32+ rows = [],
33+ cols = [],
34+ justifySelf,
35+ alignSelf,
36+ className,
37+ ...rest
38+ },
39+ ref
40+) {
3441 return (
3542 <As
43+ ref={ref}
3644 className={cn(
3745 {
3846 full,
@@ -53,7 +61,7 @@ Grid.Item = function GridItem({
5361 {...rest}
5462 />
5563 );
56−};
64+});
5765
5866Grid.Divider = function GridDivider({
5967 vertical = false,
src/indigo-react/components/Input.js+3−19
@@ -14,9 +14,6 @@ export default function Input({
1414
1515 // callbacks
1616 onValue,
17− onPass,
18− onError,
19− onFocus,
2017 onEnter,
2118
2219 // state from hook
@@ -33,7 +30,6 @@ export default function Input({
3330 initialValue,
3431 validators,
3532 transformers,
36− setValue,
3733
3834 // extra
3935 ...rest
@@ -50,25 +46,13 @@ export default function Input({
5046 );
5147
5248 // notify parent of value only when passing
49+ // TODO: deprecate/remove this
50+ // keeping it around for now to avoid refactoring InviteEmail.js, but this
51+ // feels like a hack
5352 useEffect(() => {
5453 pass && onValue && onValue(data);
5554 }, [pass, data]); // eslint-disable-line react-hooks/exhaustive-deps
5655
57− // notify parent of pass
58− useEffect(() => {
59− onPass && onPass(pass);
60− }, [pass]); // eslint-disable-line react-hooks/exhaustive-deps
61−
62− // notify parent of error whenever error changes
63− useEffect(() => {
64− onError && onError(error);
65− }, [error]); // eslint-disable-line react-hooks/exhaustive-deps
66−
67− // notify parent of focus
68− useEffect(() => {
69− onFocus && onFocus(focused);
70− }, [focused]); // eslint-disable-line react-hooks/exhaustive-deps
71−
7256 return (
7357 <Flex
7458 className={cn(className, 'mb1')}
src/indigo-react/components/SelectInput.jsnew file+147
@@ -0,0 +1,147 @@
1+import React, { useCallback, useRef, useState } from 'react';
2+import cn from 'classnames';
3+
4+import Flex from './Flex';
5+import { ErrorText } from './Typography';
6+import useOnClickOutside from 'indigo-react/lib/useOnClickOutside';
7+import AccessoryIcon from './AccessoryIcon';
8+
9+export default function SelectInput({
10+ // visuals
11+ name,
12+ label,
13+ className,
14+ accessory,
15+ mono = false,
16+ placeholder,
17+
18+ // callbacks
19+ onValue,
20+ onEnter,
21+
22+ // state from hook
23+ focused,
24+ pass,
25+ visiblyPassed,
26+ error,
27+ data,
28+ bind,
29+ autoFocus,
30+ disabled,
31+ options,
32+
33+ // ignored
34+ initialValue,
35+ validators,
36+ transformers,
37+
38+ // extra
39+ ...rest
40+}) {
41+ const [isOpen, setIsOpen] = useState(false);
42+ const ref = useRef();
43+
44+ // close select on outside clicks
45+ useOnClickOutside(ref, useCallback(() => setIsOpen(false), [setIsOpen]));
46+
47+ const toggleOpen = useCallback(() => setIsOpen(isOpen => !isOpen), [
48+ setIsOpen,
49+ ]);
50+
51+ const onChange = value => {
52+ // TODO: provide setValue here?
53+ // construct a pseudo event that sets the value correctly
54+ bind.onChange({ target: { value } });
55+ setIsOpen(false);
56+ };
57+
58+ // redefine accessory because we still want to ignore it from the ..rest above
59+ accessory = <AccessoryIcon>{isOpen ? '⬆' : '⬇'}</AccessoryIcon>;
60+
61+ const text = options.find(o => o.value === data).text;
62+
63+ return (
64+ <Flex
65+ ref={ref}
66+ className={cn(className, 'mb1 us-none')}
67+ col
68+ style={{
69+ ...(disabled && {
70+ pointerEvents: 'none',
71+ cursor: 'not-allowed',
72+ }),
73+ }}>
74+ <Flex.Item as="label" className="f6 lh-tall" htmlFor={name}>
75+ {label}
76+ </Flex.Item>
77+ <Flex.Item as={Flex} row className="rel clickable" onClick={toggleOpen}>
78+ <Flex.Item
79+ // as="select"
80+ className={cn(
81+ 'b b1 p3 outline-none',
82+ { mono },
83+ {
84+ 'bg-white': !disabled,
85+ 'bg-gray1': disabled,
86+ },
87+ {
88+ gray4: isOpen,
89+ black: !isOpen,
90+ },
91+ {
92+ 'b-green3': visiblyPassed,
93+ 'b-black': focused && !visiblyPassed,
94+ 'b-yellow3': !focused && error,
95+ 'b-gray3': !focused && !error && !visiblyPassed,
96+ }
97+ )}
98+ id={name}
99+ name={name}
100+ {...bind}
101+ flex>
102+ {isOpen ? placeholder : text}
103+ </Flex.Item>
104+ <div
105+ className="abs"
106+ style={{
107+ top: 0,
108+ right: 0,
109+ height: '100%',
110+ width: '44px',
111+ overflow: 'hidden',
112+ }}>
113+ {accessory}
114+ </div>
115+ {isOpen && (
116+ <Flex
117+ col
118+ className="abs bg-white b-black b1 z10"
119+ style={{
120+ top: 'calc(100% + 4px)',
121+ left: 0,
122+ right: 0,
123+ overflow: 'auto',
124+ maxHeight: '30vh',
125+ }}>
126+ {options.map(option => (
127+ <Flex.Item
128+ key={option.value}
129+ className="pv2 ph3 hover-bg-grey3"
130+ onClick={e => {
131+ e.stopPropagation();
132+ onChange(option.value);
133+ }}>
134+ {option.text}
135+ </Flex.Item>
136+ ))}
137+ </Flex>
138+ )}
139+ </Flex.Item>
140+ {error && (
141+ <Flex.Item as={ErrorText} className="mv1">
142+ {error}
143+ </Flex.Item>
144+ )}
145+ </Flex>
146+ );
147+}
src/indigo-react/lib/useForm.js+27−21
@@ -38,7 +38,9 @@ export default function useForm(inputConfigs = []) {
3838
3939 // track values
4040 const [values, _setValues] = useState(() =>
41− defaultsFor(configs, config => config.initialValue || '')
41+ defaultsFor(configs, config =>
42+ config.initialValue === undefined ? '' : config.initialValue
43+ )
4244 );
4345 // ^ NB(shrugs): because we're not syncing the initialValue of _additional_
4446 // configs that are added, we won't be able to set an initialValue for a
@@ -164,27 +166,31 @@ export default function useForm(inputConfigs = []) {
164166 const inputs = useMemo(
165167 () =>
166168 configs.map(
167− ({ name, error, autoFocus, disabled, initialValue, ...rest }, i) => ({
168− // Input props
169− name,
170− data: datas[i],
171− pass: passes[i],
172− visiblyPassed: visiblePasses[i],
173− error: errors[i],
174− focused: focuses[i],
175− autoFocus: autoFocus && !disabled,
176− disabled,
177− ...rest,
178− // dom properties below:
179− bind: {
180− value: values[name] || initialValue || '',
181− checked: !!values[name],
182− onChange: onChange(name),
183− onFocus: onFocus(name),
184− onBlur: onBlur(name),
169+ ({ name, error, autoFocus, disabled, initialValue, ...rest }, i) => {
170+ const value =
171+ values[name] || (initialValue === undefined ? '' : initialValue);
172+ return {
173+ // Input props
174+ name,
175+ data: datas[i],
176+ pass: passes[i],
177+ visiblyPassed: visiblePasses[i],
178+ error: errors[i],
179+ focused: focuses[i],
185180 autoFocus: autoFocus && !disabled,
186− },
187− })
181+ disabled,
182+ ...rest,
183+ // dom properties below:
184+ bind: {
185+ value,
186+ checked: !!values[name],
187+ onChange: onChange(name),
188+ onFocus: onFocus(name),
189+ onBlur: onBlur(name),
190+ autoFocus: autoFocus && !disabled,
191+ },
192+ };
193+ }
188194 ),
189195 [
190196 configs,
src/indigo-react/lib/useOnClickOutside.jsnew file+24
@@ -0,0 +1,24 @@
1+import { useEffect } from 'react';
2+
3+//via https:// usehooks.com/useOnClickOutside/
4+
5+export default function useOnClickOutside(ref, handler) {
6+ useEffect(() => {
7+ const listener = event => {
8+ // Do nothing if clicking ref's element or descendent elements
9+ if (!ref.current || ref.current.contains(event.target)) {
10+ return;
11+ }
12+
13+ handler(event);
14+ };
15+
16+ document.addEventListener('mousedown', listener);
17+ document.addEventListener('touchstart', listener);
18+
19+ return () => {
20+ document.removeEventListener('mousedown', listener);
21+ document.removeEventListener('touchstart', listener);
22+ };
23+ }, [ref, handler]);
24+}
src/lib/validators.js+8−1
@@ -1,6 +1,6 @@
11import * as bip39 from 'bip39';
22import * as ob from 'urbit-ob';
3−import { identity } from 'lodash';
3+import { identity, includes } from 'lodash';
44
55import { isValidAddress } from './wallet';
66import patp2dec from './patp2dec';
@@ -149,6 +149,13 @@ export const validateLength = l => m =>
149149 error: `Must be exactly ${l} characters`,
150150 });
151151
152+export const validateOneOf = (options = []) => m =>
153+ simpleValidatorWrapper({
154+ prevMessage: m,
155+ validator: d => includes(options, d),
156+ error: `Is not a valid option.`,
157+ });
158+
152159export const validateHexString = m =>
153160 simpleValidatorWrapper({
154161 prevMessage: m,
src/style/indigo-proposals.scss+4
@@ -39,3 +39,7 @@
3939.auto-rows-min {
4040 grid-auto-rows: min-content;
4141}
42+
43+.hover-bg-grey3:hover {
44+ background-color: #e6e6e6;
45+}
src/views/Login/Ledger.js+38−36
@@ -1,4 +1,4 @@
1−import React, { useCallback, useEffect, useState } from 'react';
1+import React, { useCallback, useEffect } from 'react';
22import Maybe from 'folktale/maybe';
33import { P, Text, Input, Grid, H5, CheckboxInput } from 'indigo-react';
44import { times } from 'lodash';
@@ -8,8 +8,11 @@ import Eth from '@ledgerhq/hw-app-eth';
88import * as secp256k1 from 'secp256k1';
99
1010import { ForwardButton } from 'components/Buttons';
11−import { InnerLabelDropdown } from 'components/old/Base';
12−import { useCheckboxInput, useHdPathInput } from 'components/Inputs';
11+import {
12+ useCheckboxInput,
13+ useHdPathInput,
14+ useSelectInput,
15+} from 'components/Inputs';
1316
1417import { useWallet } from 'store/wallet';
1518
@@ -21,14 +24,15 @@ import {
2124} from 'lib/ledger';
2225import { WALLET_TYPES } from 'lib/wallet';
2326import useLoginView from 'lib/useLoginView';
27+import SelectInput from 'indigo-react/components/SelectInput';
2428
25−const pathOptions = [
26− { title: 'Ledger Live', value: LEDGER_LIVE_PATH },
27− { title: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
29+const PATH_OPTIONS = [
30+ { text: 'Ledger Live', value: LEDGER_LIVE_PATH },
31+ { text: 'Ledger Legacy', value: LEDGER_LEGACY_PATH },
2832];
2933
30−const accountOptions = times(20, i => ({
31− title: `Account #${i + 1}`,
34+const ACCOUNT_OPTIONS = times(20, i => ({
35+ text: `Account #${i + 1}`,
3236 value: i,
3337}));
3438
@@ -37,10 +41,21 @@ export default function Ledger({ className }) {
3741
3842 const { setWallet, setWalletHdPath } = useWallet();
3943
40− // ledger has two derivation path _patterns_
41− const [basePathPattern, setBasePathPattern] = useState(LEDGER_LIVE_PATH);
42− // and then we want to be able to select an index within that path
43− const [accountIndex, setAccountIndex] = useState(0);
44+ // derivation path input
45+ const [derivationPathInput, { data: basePathPattern }] = useSelectInput({
46+ name: 'derivationpath',
47+ label: 'Derivation Path',
48+ placeholder: 'Choose path pattern...',
49+ options: PATH_OPTIONS,
50+ });
51+
52+ // account input
53+ const [accountInput, { data: accountIndex }] = useSelectInput({
54+ name: 'account',
55+ label: 'Account',
56+ placeholder: 'Choose account...',
57+ options: ACCOUNT_OPTIONS,
58+ });
4459
4560 // custom toggle
4661 const [customPathInput, { data: useCustomPath }] = useCheckboxInput({
@@ -90,11 +105,6 @@ export default function Ledger({ className }) {
90105 }
91106 }, [useCustomPath, setHdPath, basePathPattern, accountIndex]);
92107
93− const basePathPatternTitle = pathOptions.find(
94− o => o.value === basePathPattern
95− ).title;
96− const accountTitle = accountOptions.find(o => o.value === accountIndex).title;
97−
98108 const isHTTPS = document.location.protocol === 'http:';
99109
100110 // when not on https, tell user how to get there
@@ -141,41 +151,33 @@ export default function Ledger({ className }) {
141151 <Grid.Item full as={H5}>
142152 Authenticate With Your Ledger
143153 </Grid.Item>
144− <Grid.Item full as={Text} className="f6">
154+ <Grid.Item full as={Text} className="f6 mb3">
145155 Connect and authenticate to your Ledger, and then open the "Ethereum"
146156 application. If you're running on older firmware, make sure the "browser
147157 support" option is turned on. To sign transactions, you'll also need to
148158 enable the "contract data" option.
149159 </Grid.Item>
150160
151− {useCustomPath && (
152− <Grid.Item full as={Input} className="mv3" {...hdPathInput} />
153− )}
161+ {useCustomPath && <Grid.Item full as={Input} {...hdPathInput} />}
154162
155163 {!useCustomPath && (
156164 <>
157165 <Grid.Item
158− full
159− as={InnerLabelDropdown}
160− className="mv4"
161− title="Derivation path"
162− options={pathOptions}
163− handleUpdate={setBasePathPattern}
164− currentSelectionTitle={basePathPatternTitle}
166+ half={1}
167+ as={SelectInput}
168+ className="pr1"
169+ {...derivationPathInput}
165170 />
166171 <Grid.Item
167− full
168− as={InnerLabelDropdown}
169− className="mt4"
170− title="Account"
171− options={accountOptions}
172− handleUpdate={setAccountIndex}
173− currentSelectionTitle={accountTitle}
172+ half={2}
173+ as={SelectInput}
174+ className="pl1"
175+ {...accountInput}
174176 />
175177 </>
176178 )}
177179
178− <Grid.Item full as={CheckboxInput} {...customPathInput} />
180+ <Grid.Item full as={CheckboxInput} className="mv3" {...customPathInput} />
179181
180182 <Grid.Item
181183 full
src/views/Login/Mnemonic.js+1−1
@@ -38,7 +38,7 @@ export default function Mnemonic({ className }) {
3838
3939 const [passphraseInput, { data: passphrase }] = usePassphraseInput({
4040 name: 'passphrase',
41− label: '(Optional) Wallet Passphrase',
41+ label: 'Wallet Passphrase',
4242 });
4343
4444 const [hdPathInput, { data: hdPath }] = useHdPathInput({
src/views/Login/Ticket.js+1−1
@@ -32,7 +32,7 @@ export default function Ticket({ className }) {
3232 // point
3333 const [pointInput, { data: pointName }] = usePointInput({
3434 name: 'point',
35− initialValue: impliedPoint,
35+ initialValue: impliedPoint || '',
3636 autoFocus: true,
3737 });
3838
src/views/Login/Trezor.js+24−28
@@ -1,4 +1,4 @@
1−import React, { useEffect, useState } from 'react';
1+import React, { useEffect } from 'react';
22import Maybe from 'folktale/maybe';
33import * as bip32 from 'bip32';
44import { times } from 'lodash';
@@ -7,17 +7,21 @@ import * as secp256k1 from 'secp256k1';
77import { Text, Input, Grid, H5, CheckboxInput } from 'indigo-react';
88
99import { ForwardButton } from 'components/Buttons';
10−import { InnerLabelDropdown } from 'components/old/Base';
1110
1211import { TREZOR_PATH } from 'lib/trezor';
1312import { WALLET_TYPES } from 'lib/wallet';
1413
1514import { useWallet } from 'store/wallet';
1615import useLoginView from 'lib/useLoginView';
17−import { useHdPathInput, useCheckboxInput } from 'components/Inputs';
18−
19−const accountOptions = times(20, i => ({
20− title: `Account #${i + 1}`,
16+import {
17+ useHdPathInput,
18+ useCheckboxInput,
19+ useSelectInput,
20+} from 'components/Inputs';
21+import SelectInput from 'indigo-react/components/SelectInput';
22+
23+const ACCOUNT_OPTIONS = times(20, i => ({
24+ text: `Account #${i + 1}`,
2125 value: i,
2226}));
2327
@@ -27,8 +31,6 @@ export default function Trezor({ className }) {
2731
2832 const { setWallet, setWalletHdPath } = useWallet();
2933
30− const [accountIndex, setAccountIndex] = useState(0);
31−
3234 // custom toggle
3335 const [customPathInput, { data: useCustomPath }] = useCheckboxInput({
3436 name: 'customPath',
@@ -37,6 +39,14 @@ export default function Trezor({ className }) {
3739 initialValue: false,
3840 });
3941
42+ // account input
43+ const [accountInput, { data: accountIndex }] = useSelectInput({
44+ name: 'account',
45+ label: 'Account',
46+ placeholder: 'Choose account...',
47+ options: ACCOUNT_OPTIONS,
48+ });
49+
4050 // hd path input
4151 const [
4252 hdPathInput,
@@ -79,36 +89,22 @@ export default function Trezor({ className }) {
7989 }
8090 }, [useCustomPath, setHdPath, accountIndex]);
8191
82− const accountTitle = accountOptions.find(o => o.value === accountIndex).title;
83−
8492 return (
8593 <Grid className={className}>
8694 <Grid.Item full as={H5}>
8795 Authenticate With Your Trezor
8896 </Grid.Item>
8997
90− <Grid.Item full as={Text} className="f6">
98+ <Grid.Item full as={Text} className="f6 mb3">
9199 Connect and authenticate to your Trezor. If you'd like to use a custom
92100 derivation path, you may enter it below.
93101 </Grid.Item>
94102
95− {useCustomPath && (
96− <Grid.Item full as={Input} className="mv3" {...hdPathInput} />
97− )}
98−
99− {!useCustomPath && (
100− <Grid.Item
101− full
102− as={InnerLabelDropdown}
103− className="mv4"
104− title="Account"
105− options={accountOptions}
106− handleUpdate={setAccountIndex}
107− currentSelectionTitle={accountTitle}
108− />
109− )}
110−
111− <Grid.Item full as={CheckboxInput} {...customPathInput} />
103+ {useCustomPath && <Grid.Item full as={Input} {...hdPathInput} />}
104+
105+ {!useCustomPath && <Grid.Item full as={SelectInput} {...accountInput} />}
106+
107+ <Grid.Item full as={CheckboxInput} className="mv3" {...customPathInput} />
112108
113109 <Grid.Item
114110 full