preact#Fragment TypeScript Examples

The following examples show how to use preact#Fragment. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: ConsentCheckboxLabel.tsx    From adyen-web with MIT License 6 votes vote down vote up
export default function ConsentCheckboxLabel(props: ConsentCheckboxLabelProps) {
    const { i18n } = useCoreContext();
    const linkText = i18n.get('paymentConditions');
    const translationString = i18n.get('afterPay.agreement');
    const [textBeforeLink, textAfterLink] = translationString.split('%@');

    if (textBeforeLink && textAfterLink) {
        return (
            <Fragment>
                {textBeforeLink}
                <a className="adyen-checkout__link" target="_blank" rel="noopener noreferrer" href={props.url}>
                    {linkText}
                </a>
                {textAfterLink}
            </Fragment>
        );
    }

    return <span className="adyen-checkout__checkbox__label">{i18n.get('privacyPolicy')}</span>;
}
Example #2
Source File: text.component.tsx    From passwords-fountain with MIT License 6 votes vote down vote up
Text: TypedComponent<Props> = ({
    children,
    withMarkup,
}: Props): VNode<string> => {
    useContext(LocalisationContext);

    return withMarkup ? (
        <span
            className="sanitized-translation"
            dangerouslySetInnerHTML={{
                __html: DOMPurify.sanitize(i18n._(children), {
                    ALLOWED_ATTR: ['rel', 'href', 'target'],
                }),
            }}
        />
    ) : (
        <Fragment>{i18n._(children)}</Fragment>
    );
}
Example #3
Source File: prompt.spec.tsx    From passwords-fountain with MIT License 6 votes vote down vote up
describe('Prompt', () => {
    it('should render correctly', () => {
        const { asFragment } = render(
            <Prompt
                renderContent={(): string =>
                    'Are you sure you want to delete this file?'
                }
                renderControls={(): VNode => (
                    <Fragment>
                        <Button
                            onClick={(): void => {
                                // empty
                            }}
                        >
                            Cancel
                        </Button>
                        <Button
                            onClick={(): void => {
                                // empty
                            }}
                        >
                            Ok
                        </Button>
                    </Fragment>
                )}
            />
        );

        expect(asFragment()).toMatchSnapshot();
    });
});
Example #4
Source File: tr.tsx    From gridjs with MIT License 6 votes vote down vote up
private getChildren(): ComponentChildren {
    if (this.props.children) {
      return this.props.children;
    } else {
      return (
        <Fragment>
          {this.props.row.cells.map((cell: Cell, i) => {
            const column = this.getColumn(i);

            if (column && column.hidden) return null;

            return (
              <TD
                key={cell.id}
                cell={cell}
                row={this.props.row}
                column={column}
              />
            );
          })}
        </Fragment>
      );
    }
  }
Example #5
Source File: pagination.tsx    From gridjs with MIT License 6 votes vote down vote up
renderSummary() {
    return (
      <Fragment>
        {this.props.summary && this.state.total > 0 && (
          <div
            role="status"
            aria-live="polite"
            className={classJoin(
              className('summary'),
              this.config.className.paginationSummary,
            )}
            title={this._(
              'pagination.navigate',
              this.state.page + 1,
              this.pages,
            )}
          >
            {this._('pagination.showing')}{' '}
            <b>{this._(`${this.state.page * this.state.limit + 1}`)}</b>{' '}
            {this._('pagination.to')}{' '}
            <b>
              {this._(
                `${Math.min(
                  (this.state.page + 1) * this.state.limit,
                  this.state.total,
                )}`,
              )}
            </b>{' '}
            {this._('pagination.of')} <b>{this._(`${this.state.total}`)}</b>{' '}
            {this._('pagination.results')}
          </div>
        )}
      </Fragment>
    );
  }
Example #6
Source File: prompt.story.tsx    From passwords-fountain with MIT License 6 votes vote down vote up
defaultView = (): VNode => (
    <Prompt
        renderContent={(): string =>
            'Are you sure you want to delete this file?'
        }
        renderControls={(): VNode => (
            <Fragment>
                <Button
                    onClick={(): void => {
                        // empty
                    }}
                >
                    Cancel
                </Button>
                <Button
                    onClick={(): void => {
                        // empty
                    }}
                >
                    Ok
                </Button>
            </Fragment>
        )}
    />
)
Example #7
Source File: plugin.ts    From gridjs with MIT License 6 votes vote down vote up
render() {
    if (this.props.pluginId) {
      // render a single plugin
      const plugin = this.config.plugin.get(this.props.pluginId);

      if (!plugin) return null;

      return h(
        Fragment,
        {},
        h(plugin.component, {
          plugin: plugin,
          ...plugin.props,
          ...this.props.props,
        }),
      );
    } else if (this.props.position !== undefined) {
      // render using a specific plugin position
      return h(
        Fragment,
        {},
        this.config.plugin
          .list(this.props.position)
          .map((p) =>
            h(p.component, { plugin: p, ...p.props, ...this.props.props }),
          ),
      );
    }

    return null;
  }
Example #8
Source File: task-view.tsx    From obsidian-dataview with MIT License 6 votes vote down vote up
/** JSX component which recursively renders grouped tasks. */
function TaskGrouping({ items, sourcePath }: { items: Grouping<SListItem>; sourcePath: string }) {
    const isGrouping = items.length > 0 && Groupings.isGrouping(items);

    return (
        <Fragment>
            {isGrouping &&
                items.map(item => (
                    <Fragment>
                        <h4>
                            <Lit value={item.key} sourcePath={sourcePath} />
                            <span class="dataview small-text">&nbsp;({Groupings.count(item.rows)})</span>
                        </h4>
                        <div class="dataview result-group">
                            <TaskGrouping items={item.rows} sourcePath={sourcePath} />
                        </div>
                    </Fragment>
                ))}
            {!isGrouping && <TaskList items={items as SListItem[]} />}
        </Fragment>
    );
}
Example #9
Source File: RedirectButton.tsx    From adyen-web with MIT License 6 votes vote down vote up
function RedirectButton({ payButton, onSubmit, amount = null, name, ...props }) {
    const { i18n } = useCoreContext();
    const [status, setStatus] = useState('ready');

    this.setStatus = newStatus => {
        setStatus(newStatus);
    };

    const payButtonLabel = () => {
        const isZeroAuth = amount && {}.hasOwnProperty.call(amount, 'value') && amount.value === 0;
        if (isZeroAuth) return `${i18n.get('preauthorizeWith')} ${name}`;
        return `${i18n.get('continueTo')} ${name}`;
    };

    return (
        <Fragment>
            {payButton({
                ...props,
                status,
                classNameModifiers: ['standalone'],
                label: payButtonLabel(),
                onClick: onSubmit
            })}
        </Fragment>
    );
}
Example #10
Source File: ReadOnlyPersonalDetails.tsx    From adyen-web with MIT License 6 votes vote down vote up
ReadOnlyPersonalDetails = ({ data }) => {
    const { firstName, lastName, shopperEmail, telephoneNumber }: ReadOnlyPersonalDetailsProps = data;

    return (
        <Fieldset classNameModifiers={['personalDetails']} label="personalDetails" readonly>
            {firstName && `${firstName} `}
            {lastName && `${lastName} `}
            {shopperEmail && (
                <Fragment>
                    <br />
                    {shopperEmail}
                </Fragment>
            )}
            {telephoneNumber && (
                <Fragment>
                    <br />
                    {telephoneNumber}
                </Fragment>
            )}
        </Fieldset>
    );
}
Example #11
Source File: InstantPaymentMethods.tsx    From adyen-web with MIT License 6 votes vote down vote up
function InstantPaymentMethods({ paymentMethods }: InstantPaymentMethodsProps) {
    const { i18n } = useCoreContext();

    return (
        <Fragment>
            <ul className="adyen-checkout__instant-payment-methods-list">
                {paymentMethods.map(pm => (
                    <li key={pm.type}>{pm.render()}</li>
                ))}
            </ul>
            <ContentSeparator label={i18n.get('orPayWith')} />
        </Fragment>
    );
}
Example #12
Source File: list-view.tsx    From obsidian-dataview with MIT License 5 votes vote down vote up
/** Pure view over list elements.  */
export function ListView({ query, sourcePath }: { query: Query; sourcePath: string }) {
    let context = useContext(DataviewContext);

    let items = useIndexBackedState<ListViewState>(
        context.container,
        context.app,
        context.settings,
        context.index,
        { state: "loading" },
        async () => {
            let result = await asyncTryOrPropogate(() =>
                executeList(query, context.index, sourcePath, context.settings)
            );
            if (!result.successful) return { state: "error", error: result.error, sourcePath };

            let showId = (query.header as ListQuery).showId;
            let showValue = !!(query.header as ListQuery).format;
            let mode = showId && showValue ? "both" : showId ? "id" : "value";

            return { state: "ready", items: result.value.data, mode: mode as ListMode };
        }
    );

    if (items.state == "loading")
        return (
            <Fragment>
                <ErrorPre>Loading...</ErrorPre>
            </Fragment>
        );
    else if (items.state == "error")
        return (
            <Fragment>
                {" "}
                <ErrorPre>Dataview: {items.error}</ErrorPre>{" "}
            </Fragment>
        );

    if (items.items.length == 0 && context.settings.warnOnEmptyResult)
        return <ErrorMessage message="Dataview: No results to show for list query." />;

    return <ListGrouping items={items.items} sourcePath={sourcePath} mode={items.mode} />;
}
Example #13
Source File: table-view.tsx    From obsidian-dataview with MIT License 5 votes vote down vote up
/** Simple table over headings and corresponding values. */
export function TableGrouping({
    headings,
    values,
    sourcePath,
}: {
    headings: string[];
    values: Literal[][];
    sourcePath: string;
}) {
    let settings = useContext(DataviewContext).settings;

    return (
        <Fragment>
            <table class="dataview table-view-table">
                <thead class="table-view-thead">
                    <tr class="table-view-tr-header">
                        {headings.map((heading, index) => (
                            <th class="table-view-th">
                                <Markdown sourcePath={sourcePath} content={heading} />
                                {index == 0 && <span class="dataview small-text">&nbsp;({values.length})</span>}
                            </th>
                        ))}
                    </tr>
                </thead>
                <tbody class="table-view-tbody">
                    {values.map(row => (
                        <tr>
                            {row.map(element => (
                                <td>
                                    <Lit value={element} sourcePath={sourcePath} />
                                </td>
                            ))}
                        </tr>
                    ))}
                </tbody>
            </table>
            {settings.warnOnEmptyResult && values.length == 0 && (
                <ErrorMessage message="Dataview: No results to show for table query." />
            )}
        </Fragment>
    );
}
Example #14
Source File: table-view.tsx    From obsidian-dataview with MIT License 5 votes vote down vote up
/** Pure view over list elements.  */
export function TableView({ query, sourcePath }: { query: Query; sourcePath: string }) {
    let context = useContext(DataviewContext);

    let items = useIndexBackedState<TableViewState>(
        context.container,
        context.app,
        context.settings,
        context.index,
        { state: "loading" },
        async () => {
            let result = await asyncTryOrPropogate(() =>
                executeTable(query, context.index, sourcePath, context.settings)
            );
            if (!result.successful) return { state: "error", error: result.error };

            let showId = (query.header as TableQuery).showId;
            if (showId) {
                let dataWithNames: Literal[][] = [];
                for (let entry of result.value.data) dataWithNames.push([entry.id].concat(entry.values));

                let name =
                    result.value.idMeaning.type === "group"
                        ? result.value.idMeaning.name
                        : context.settings.tableIdColumnName;

                return { state: "ready", headings: [name].concat(result.value.names), values: dataWithNames };
            }

            // Do not append the ID field by default.
            return { state: "ready", headings: result.value.names, values: result.value.data.map(v => v.values) };
        }
    );

    if (items.state == "loading")
        return (
            <Fragment>
                <ErrorPre>Loading...</ErrorPre>
            </Fragment>
        );
    else if (items.state == "error")
        return (
            <Fragment>
                {" "}
                <ErrorPre>Dataview: {items.error}</ErrorPre>{" "}
            </Fragment>
        );

    return <TableGrouping headings={items.headings} values={items.values} sourcePath={sourcePath} />;
}
Example #15
Source File: task-view.tsx    From obsidian-dataview with MIT License 5 votes vote down vote up
/**
 * Pure view over (potentially grouped) tasks and list items which allows for checking/unchecking tasks and manipulating
 * the task view.
 */
export function TaskView({ query, sourcePath }: { query: Query; sourcePath: string }) {
    let context = useContext(DataviewContext);

    let items = useIndexBackedState<TaskViewState>(
        context.container,
        context.app,
        context.settings,
        context.index,
        { state: "loading" },
        async () => {
            let result = await asyncTryOrPropogate(() =>
                executeTask(query, sourcePath, context.index, context.settings)
            );
            if (!result.successful) return { state: "error", error: result.error, sourcePath };
            else return { state: "ready", items: result.value.tasks };
        }
    );

    if (items.state == "loading")
        return (
            <Fragment>
                <ErrorPre>Loading</ErrorPre>
            </Fragment>
        );
    else if (items.state == "error")
        return (
            <Fragment>
                <ErrorPre>Dataview: {items.error}</ErrorPre>
            </Fragment>
        );

    return (
        <div class="dataview dataview-container">
            <TaskGrouping items={items.items} sourcePath={sourcePath} />
        </div>
    );
}
Example #16
Source File: IssuerList.tsx    From adyen-web with MIT License 5 votes vote down vote up
function IssuerList({ items, placeholder = 'idealIssuer.selectField.placeholder', issuer, highlightedIds = [], ...props }: IssuerListProps) {
    const { i18n } = useCoreContext();
    const { handleChangeFor, triggerValidation, data, valid, errors, isValid } = useForm({
        schema,
        defaultData: { issuer },
        rules: validationRules
    });
    const [status, setStatus] = useState('ready');
    const [inputType, setInputType] = useState<IssuerListInputTypes>(IssuerListInputTypes.Dropdown);

    this.setStatus = newStatus => {
        setStatus(newStatus);
    };

    const handleInputChange = useCallback(
        (type: IssuerListInputTypes) => (event: UIEvent) => {
            setInputType(type);
            handleChangeFor('issuer')(event);
        },
        [handleChangeFor]
    );

    useEffect(() => {
        props.onChange({ data, valid, errors, isValid });
    }, [data, valid, errors, isValid]);

    this.showValidation = () => {
        triggerValidation();
    };

    const { highlightedItems } = items.reduce(
        (memo, item) => {
            if (highlightedIds.includes(item.id)) memo.highlightedItems.push({ ...item });
            return memo;
        },
        { highlightedItems: [] }
    );

    return (
        <div className="adyen-checkout__issuer-list">
            {!!highlightedItems.length && (
                <Fragment>
                    <IssuerButtonGroup
                        selectedIssuerId={inputType === IssuerListInputTypes.ButtonGroup ? data['issuer'] : null}
                        items={highlightedItems}
                        onChange={handleInputChange(IssuerListInputTypes.ButtonGroup)}
                    />
                    <ContentSeparator />
                </Fragment>
            )}

            <Field errorMessage={!!errors['issuer']} classNameModifiers={['issuer-list']}>
                {renderFormField('select', {
                    items,
                    selected: inputType === IssuerListInputTypes.Dropdown ? data['issuer'] : null,
                    placeholder: i18n.get(placeholder),
                    name: 'issuer',
                    className: 'adyen-checkout__issuer-list__dropdown',
                    onChange: handleInputChange(IssuerListInputTypes.Dropdown)
                })}
            </Field>

            {props.showPayButton &&
                props.payButton({
                    status,
                    label: payButtonLabel({ issuer: data['issuer'], items: [...items, ...highlightedItems] }, i18n)
                })}
        </div>
    );
}
Example #17
Source File: SelectButton.tsx    From adyen-web with MIT License 5 votes vote down vote up
function SelectButton(props: SelectButtonProps) {
    const { i18n } = useCoreContext();
    const { active, readonly, showList } = props;

    return (
        <SelectButtonElement
            aria-disabled={readonly}
            aria-expanded={showList}
            aria-haspopup="listbox"
            className={cx({
                'adyen-checkout__dropdown__button': true,
                [styles['adyen-checkout__dropdown__button']]: true,
                'adyen-checkout__dropdown__button--readonly': readonly,
                'adyen-checkout__dropdown__button--active': showList,
                [styles['adyen-checkout__dropdown__button--active']]: showList,
                'adyen-checkout__dropdown__button--invalid': props.isInvalid,
                'adyen-checkout__dropdown__button--valid': props.isValid
            })}
            filterable={props.filterable}
            onClick={!readonly ? props.toggleList : null}
            onKeyDown={!readonly ? props.onButtonKeyDown : null}
            role={props.filterable ? 'button' : null}
            tabIndex="0"
            title={active.name || props.placeholder}
            toggleButtonRef={props.toggleButtonRef}
            type={!props.filterable ? 'button' : null}
            aria-describedby={props.ariaDescribedBy}
            id={props.id}
        >
            {!showList || !props.filterable ? (
                <Fragment>
                    <span className="adyen-checkout__dropdown__button__text">{active.selectedOptionName || active.name || props.placeholder}</span>
                    {active.icon && <Img className="adyen-checkout__dropdown__button__icon" src={active.icon} alt={active.name} />}
                </Fragment>
            ) : (
                <input
                    aria-autocomplete="list"
                    aria-controls={props.selectListId}
                    aria-expanded={showList}
                    aria-owns={props.selectListId}
                    autoComplete="off"
                    className={cx('adyen-checkout__filter-input', [styles['adyen-checkout__filter-input']])}
                    onInput={props.onInput}
                    placeholder={i18n.get('select.filter.placeholder')}
                    ref={props.filterInputRef}
                    role="combobox"
                    type="text"
                />
            )}
        </SelectButtonElement>
    );
}
Example #18
Source File: PaymentMethodList.tsx    From adyen-web with MIT License 5 votes vote down vote up
render({ paymentMethods, instantPaymentMethods, activePaymentMethod, cachedPaymentMethods, isLoading }) {
        const paymentMethodListClassnames = classNames({
            [styles['adyen-checkout__payment-methods-list']]: true,
            'adyen-checkout__payment-methods-list': true,
            'adyen-checkout__payment-methods-list--loading': isLoading
        });

        return (
            <Fragment>
                {this.props.orderStatus && (
                    <OrderPaymentMethods order={this.props.order} orderStatus={this.props.orderStatus} onOrderCancel={this.props.onOrderCancel} />
                )}

                {!!instantPaymentMethods.length && <InstantPaymentMethods paymentMethods={instantPaymentMethods} />}

                <ul className={paymentMethodListClassnames}>
                    {paymentMethods.map((paymentMethod, index, paymentMethodsCollection) => {
                        const isSelected = activePaymentMethod && activePaymentMethod._id === paymentMethod._id;
                        const isLoaded = paymentMethod._id in cachedPaymentMethods;
                        const isNextOneSelected =
                            activePaymentMethod &&
                            paymentMethodsCollection[index + 1] &&
                            activePaymentMethod._id === paymentMethodsCollection[index + 1]._id;

                        return (
                            <PaymentMethodItem
                                className={classNames({ 'adyen-checkout__payment-method--next-selected': isNextOneSelected })}
                                standalone={paymentMethods.length === 1}
                                paymentMethod={paymentMethod}
                                isSelected={isSelected}
                                isDisabling={isSelected && this.props.isDisabling}
                                isLoaded={isLoaded}
                                isLoading={isLoading}
                                onSelect={this.onSelect(paymentMethod)}
                                key={paymentMethod._id}
                                showRemovePaymentMethodButton={this.props.showRemovePaymentMethodButton}
                                onDisableStoredPaymentMethod={this.props.onDisableStoredPaymentMethod}
                            />
                        );
                    })}
                </ul>
            </Fragment>
        );
    }
Example #19
Source File: Faq.tsx    From help-widget with MIT License 5 votes vote down vote up
Faq = () => {
    const service = useContext(ServiceContext);
    const [questions, setQuestions] = useState<FaqModel[] | undefined>(undefined);
    const [visible, setVisible] = useState(0);
    const [statusText, setStatusText] = useState('');

    const loaders = [
        useTimeout(() => !questions && setStatusText('Loading...'), 500),
        useTimeout(() => !questions && setStatusText('Still loading...'), 5000),
        useTimeout(() => !questions && setStatusText('Backend still didn\'t return results...'), 10000)];

    useEffect(() => {
        service?.getFaq()
            .then(setQuestions)
            .catch(() => setStatusText('Failed to load, try again later.'))
            .then(() => loaders.forEach((c) => c()));
    }, [service]);

    return (
        <div>
            {
                !questions
                    ? statusText
                    : <Fragment>
                        <p>
                            Here is a list of frequently asked questions.
                            You can also contact us <RouteLink href='/'> here</RouteLink>.
                        </p>
                        <ul className={style.root}>
                            {
                                questions.map((q, i) => (
                                    <li key={i} className={clsx({ [style.visible]: i === visible })}>
                                        <a href='javascript:;' onClick={() => setVisible(i)}>{q.question}</a>
                                        <span>{q.answer}</span>
                                    </li>))
                            }
                        </ul>
                    </Fragment>
            }
        </div>
    );
}
Example #20
Source File: optionsPanelEntityFormExpanded.component.tsx    From passwords-fountain with MIT License 4 votes vote down vote up
OptionsPanelEntityFormExpanded: TypedComponent<VariantProps> = ({
    switchCurrentVariantName,
}: VariantProps) => {
    const encryptedAdminKey = useSelector(selectAdminKey);
    const formRef = useRef(undefined as any);
    const firstInputRef = useRef(undefined as any);
    const addNewPassword = useAction(passwordListActions.addNewPassword);
    const editPassword = useAction(passwordListActions.editPassword);
    const fetchPasswords = useAction(passwordListActions.fetchPasswords);

    const editedEntity = useSelector(selectSelectedAndDecryptedEntity);
    const isInEditMode = useSelector(selectIsInEditMode);
    const actionLabel = isInEditMode ? 'optionsPanel.edit' : 'optionsPanel.add';

    useEffect(() => {
        firstInputRef.current.base.focus();
    }, [firstInputRef]);

    const useInputForm = (fieldName: string, defaultValue?: string) =>
        useInputFormControl(formRef, formValidation, fieldName, defaultValue);

    const [labelInputState, labelInputProps] = useInputForm(
        'label',
        editedEntity.label
    );
    const [loginInputState, loginInputProps] = useInputForm(
        'login',
        editedEntity.login
    );
    const [passwordInputState, passwordInputProps] = useInputForm(
        'password',
        editedEntity.password
    );
    const [encryptionKeyInputState, encryptionKeyInputProps] = useInputForm(
        'encryptionKey'
    );

    const handleCancelClick = (): void =>
        switchCurrentVariantName(optionsPanelVariantNames.entityFormCollapsed);

    const handleAction = async (e: Event): Promise<void> => {
        e.preventDefault();

        if (!formRef.current?.isValid) {
            return;
        }

        if (isInEditMode) {
            await editPassword(
                {
                    label: labelInputState.value,
                    login: loginInputState.value,
                    password: passwordInputState.value,
                    refId: editedEntity.refId,
                },
                encryptionKeyInputState.value
            );
        } else {
            await addNewPassword(
                {
                    label: labelInputState.value,
                    login: loginInputState.value,
                    password: passwordInputState.value,
                },
                encryptionKeyInputState.value
            );
        }
        fetchPasswords(encryptionKeyInputState.value, encryptedAdminKey);
    };

    const renderError = (errors: string) => (): VNode => <Text>{errors}</Text>;
    const renderLabel = (label: string, noteText?: string) => (): VNode => (
        <Fragment>
            <Text>{label}</Text>
            {renderIfTrue(() => (
                <NoteLabelWrapper>
                    <Text>settings.noteLabel</Text>{' '}
                    <Text>{noteText as string}</Text>
                </NoteLabelWrapper>
            ))(Boolean(noteText))}
        </Fragment>
    );
    return (
        <Wrapper>
            <ContentWrapper>
                <Content>
                    <form ref={formRef} onSubmit={handleAction}>
                        <FormControlWrapper>
                            <FormControl
                                id={labelInputProps.name}
                                hasError={labelInputProps.hasError}
                                renderLabel={renderLabel(
                                    'optionsPanel.labelInputLabel'
                                )}
                                renderInput={(id: string): VNode => (
                                    <TextInput
                                        ref={firstInputRef}
                                        id={id}
                                        placeholder="e.g. My Bank Account"
                                        {...labelInputProps}
                                    />
                                )}
                                renderError={renderError(
                                    labelInputState.errors
                                )}
                            />
                        </FormControlWrapper>
                        <FormControlWrapper>
                            <FormControl
                                id={loginInputProps.name}
                                hasError={loginInputProps.hasError}
                                renderLabel={renderLabel(
                                    'optionsPanel.loginInputLabel'
                                )}
                                renderInput={(id: string): VNode => (
                                    <TextInput
                                        id={id}
                                        placeholder="e.g. [email protected]"
                                        {...loginInputProps}
                                    />
                                )}
                                renderError={renderError(
                                    loginInputState.errors
                                )}
                            />
                        </FormControlWrapper>
                        <FormControlWrapper>
                            <FormControl
                                id={passwordInputProps.name}
                                hasError={passwordInputProps.hasError}
                                renderLabel={renderLabel(
                                    'optionsPanel.passwordInputLabel'
                                )}
                                renderInput={(id: string): VNode => (
                                    <TextInput
                                        id={id}
                                        type="password"
                                        placeholder="e.g. myPassWord1234"
                                        {...passwordInputProps}
                                    />
                                )}
                                renderError={renderError(
                                    passwordInputState.errors
                                )}
                            />
                        </FormControlWrapper>
                        <FormControlWrapper>
                            <FormControl
                                id={encryptionKeyInputProps.name}
                                hasError={encryptionKeyInputProps.hasError}
                                renderLabel={renderLabel(
                                    'optionsPanel.encryptionKey',
                                    'optionsPanel.noteEncryptionKey'
                                )}
                                renderInput={(id: string): VNode => (
                                    <TextInput
                                        id={id}
                                        type="password"
                                        placeholder="e.g. MyStrongPassword1234"
                                        {...encryptionKeyInputProps}
                                    />
                                )}
                                renderError={renderError(
                                    encryptionKeyInputState.errors
                                )}
                            />
                        </FormControlWrapper>
                        <input type="submit" hidden />
                    </form>
                </Content>
            </ContentWrapper>
            <ButtonWrapper>
                <Button onClick={handleCancelClick}>
                    <Text>optionsPanel.cancel</Text>
                </Button>
                <Button
                    onClick={handleAction}
                    disabled={!formRef.current?.isValid}
                >
                    <Text>{actionLabel}</Text>
                </Button>
            </ButtonWrapper>
        </Wrapper>
    );
}
Example #21
Source File: passwordEntity.component.tsx    From passwords-fountain with MIT License 4 votes vote down vote up
PasswordEntity: TypedComponent<Props> = ({
    data,
    isSelected,
    onClick,
}: Props) => {
    const formRef = useRef(undefined as any);
    const promptInputRef = useRef(undefined as any);
    const [promptType, setPromptType] = useState<PromptType>(
        promptTypes.invisible
    );
    const encryptedAdminKey = useSelector(selectAdminKey);
    const selectedAndDecryptedEntity = useSelector(
        selectSelectedAndDecryptedEntityByRefId(data.ref.id)
    );
    const passwordVisibility =
        Object.keys(selectedAndDecryptedEntity).length !== 0;

    const [
        encryptionKeyInputState,
        encryptionKeyInputProps,
    ] = useInputFormControl(formRef, formValidation, 'encryptionKey');

    const removePassword = useAction(passwordListActions.removePassword);
    const fetchPasswords = useAction(passwordListActions.fetchPasswords);

    const setSelectedAndDecryptedEntity = useAction(
        passwordListActions.setSelectedAndDecryptedEntity
    );
    const resetSelectedAndDecryptedEntity = useAction(
        passwordListActions.resetSelectedAndDecryptedEntity
    );

    useEffect(() => {
        promptInputRef.current?.base?.focus();
    }, [promptInputRef, promptType]);

    const resetPromptState = (): void => {
        setPromptType(promptTypes.invisible);
        encryptionKeyInputState.setValue('');
        encryptionKeyInputState.setErrors('');
    };

    const handleClick = (e: MouseEvent): void => {
        if (e.detail === 0) {
            // Firefox fix - ignore outclicking via ENTER key
            return;
        }

        onClick(isSelected ? null : data);
        resetSelectedAndDecryptedEntity();
        resetPromptState();
    };

    const handleDecryptionPromptConfirm = async (): Promise<void> => {
        const { decrypt } = await import('@/modules/cipher/cipher.service');
        const { login, password } = decrypt(
            data.data.value,
            encryptionKeyInputState.value,
            true
        ) as PasswordEntityVulnerablePayload;

        setSelectedAndDecryptedEntity({
            refId: data.ref.id,
            label: data.data.label,
            login,
            password,
        });
    };

    const handleRemovalPromptConfirm = async (): Promise<void> => {
        const { decrypt } = await import('@/modules/cipher/cipher.service');
        // only to check if master key is known - not needed to removal operation itself
        decrypt(
            data.data.value,
            encryptionKeyInputState.value,
            true
        ) as PasswordEntityVulnerablePayload;

        await removePassword(data.ref.id);
        fetchPasswords(encryptionKeyInputState.value, encryptedAdminKey);
    };

    const handlePromptConfirm = (e: Event): void => {
        e.preventDefault();

        if (!formRef.current?.isValid) {
            return;
        }

        if (promptType === promptTypes.decryption) {
            handleDecryptionPromptConfirm();
        } else {
            handleRemovalPromptConfirm();
        }

        resetPromptState();
    };

    const handleControlClick = (nextPromptType: PromptType) => (
        e: Event
    ): void => {
        e.stopPropagation();
        if (promptType) {
            return;
        }

        setPromptType(nextPromptType);
    };

    const handleFilledEyeClick = (e: Event): void => {
        e.stopPropagation();
        resetSelectedAndDecryptedEntity();
    };

    const renderPrompt = renderIfTrue(() => {
        const confirmBtnLabel =
            promptType === promptTypes.decryption
                ? 'prompt.decrypt'
                : 'prompt.remove';
        return (
            <Prompt
                renderContent={() => (
                    <form ref={formRef} onSubmit={handlePromptConfirm}>
                        <FormControlWrapper>
                            <FormControl
                                id={encryptionKeyInputProps.name}
                                hasError={encryptionKeyInputProps.hasError}
                                renderLabel={() => (
                                    <Text>optionsPanel.enterEncryptionKey</Text>
                                )}
                                renderError={() => (
                                    <Text>
                                        {encryptionKeyInputState.errors}
                                    </Text>
                                )}
                                renderInput={(id: string) => (
                                    <TextInput
                                        ref={promptInputRef}
                                        id={id}
                                        type="password"
                                        placeholder="e.g. MyStrongPassword1234"
                                        {...encryptionKeyInputProps}
                                    />
                                )}
                            />
                        </FormControlWrapper>
                    </form>
                )}
                renderControls={() => (
                    <Fragment>
                        <Button onClick={resetPromptState}>
                            <Text>optionsPanel.cancel</Text>
                        </Button>
                        <Button
                            onClick={handlePromptConfirm}
                            disabled={!formRef.current?.isValid}
                        >
                            <Text>{confirmBtnLabel}</Text>
                        </Button>
                    </Fragment>
                )}
            />
        );
    });

    const renderEyeIcon = () => {
        if (passwordVisibility) {
            return (
                <IconButton
                    iconName="eyeFilled"
                    onClick={handleFilledEyeClick}
                />
            );
        }
        return (
            <IconButton
                iconName="eye"
                onClick={handleControlClick(promptTypes.decryption)}
            />
        );
    };

    const renderControls = renderIfTrue(() => {
        return (
            <Fragment>
                <IconButton
                    iconName="bin"
                    onClick={handleControlClick(promptTypes.removal)}
                />
                {renderEyeIcon()}
            </Fragment>
        );
    });

    return (
        <Wrapper onClick={handleClick} isSelected={isSelected}>
            <GridWrapper>
                <DataWrapper>
                    <Row>
                        <Label>
                            <Text>passwordEntity.label</Text>
                        </Label>{' '}
                        - <Value>{data.data.label}</Value>
                    </Row>
                    <Row>
                        <Label>
                            <Text>passwordEntity.login</Text>
                        </Label>{' '}
                        -{' '}
                        <Value>
                            {selectedAndDecryptedEntity.login ??
                                placeholderEntityValue}
                        </Value>
                    </Row>
                    <Row>
                        <Label>
                            <Text>passwordEntity.password</Text>
                        </Label>{' '}
                        -{' '}
                        <Value>
                            {selectedAndDecryptedEntity.password ??
                                placeholderEntityValue}
                        </Value>
                    </Row>
                </DataWrapper>
                <ControlsWrapper>{renderControls(isSelected)}</ControlsWrapper>
            </GridWrapper>
            {renderPrompt(Boolean(promptType) && isSelected)}
        </Wrapper>
    );
}
Example #22
Source File: settings.tsx    From passwords-fountain with MIT License 4 votes vote down vote up
Settings: TypedComponent<Props> = () => {
    const isFirstTimeOnDevice = useSelector(selectIsFirstTimeOnDevice);
    const fetchPasswords = useAction(passwordListActions.fetchPasswords);
    const formRef = useRef<HTMLFormElement>(undefined as any);
    const [adminKeyInputState, adminKeyInputProps] = useInputFormControl(
        formRef,
        formValidation,
        'adminKey'
    );
    const [masterKeyInputState, masterKeyInputProps] = useInputFormControl(
        formRef,
        formValidation,
        'masterKey'
    );

    const headingText = isFirstTimeOnDevice
        ? 'settings.connectToDB'
        : 'settings.headingText';

    const handleConnectClick = async (e: Event): Promise<void> => {
        e.preventDefault();

        if (!formRef.current?.isValid) {
            return;
        }

        await fetchPasswords(
            masterKeyInputState.value,
            adminKeyInputState.value,
            true
        );
        route('/app');
    };
    const handleBackClick = (): void => {
        history.back();
    };

    const renderNoteLabel = (labelDescription: string, shouldRender: boolean) =>
        renderIfTrue(() => (
            <NoteLabelWrapper>
                <Text>settings.noteLabel</Text>{' '}
                <DescriptiveText>
                    <Text>{labelDescription}</Text>
                </DescriptiveText>
            </NoteLabelWrapper>
        ))(shouldRender);

    const renderLabel = (
        label: string,
        labelDescription: string,
        noteLabelDescription: string,
        shouldRenderNote = false
    ) => (): VNode => {
        return (
            <Fragment>
                <LabelWrapper>
                    <Text>{label}</Text> -{' '}
                    <DescriptiveText>
                        <Text>{labelDescription}</Text>
                    </DescriptiveText>
                </LabelWrapper>
                {renderNoteLabel(noteLabelDescription, shouldRenderNote)}
            </Fragment>
        );
    };
    const renderError = (error: string) => (): VNode => <Text>{error}</Text>;

    return (
        <Wrapper>
            <Header>
                <Heading>
                    <Text>{headingText}</Text>
                </Heading>
            </Header>
            <FormWrapper>
                <form ref={formRef}>
                    <FormControlWrapper>
                        <FormControl
                            id={adminKeyInputProps.name}
                            hasError={Boolean(adminKeyInputProps.hasError)}
                            renderLabel={renderLabel(
                                'settings.adminKeyLabel',
                                'settings.adminKeyLabelDescription',
                                'settings.noteLabelDescriptionAdminKey'
                            )}
                            renderInput={(id: string): VNode => (
                                <TextInput
                                    id={id}
                                    type="password"
                                    placeholder="92xIJf_ge234kalfnqql4o25ou4334201"
                                    {...adminKeyInputProps}
                                />
                            )}
                            renderError={renderError(adminKeyInputState.errors)}
                        />
                    </FormControlWrapper>
                    <FormControlWrapper>
                        <FormControl
                            id={masterKeyInputProps.name}
                            hasError={Boolean(masterKeyInputProps.hasError)}
                            renderLabel={renderLabel(
                                'settings.masterKeyLabel',
                                'settings.masterKeyLabelDescription',
                                'settings.noteLabelDescription',
                                true
                            )}
                            renderInput={(id: string): VNode => (
                                <TextInput
                                    id={id}
                                    type="password"
                                    placeholder="myMasterPassword1234"
                                    {...masterKeyInputProps}
                                />
                            )}
                            renderError={renderError(
                                masterKeyInputState.errors
                            )}
                        />
                    </FormControlWrapper>
                    <ControlsWrapper>
                        <Button onClick={handleBackClick}>
                            <Text>settings.back</Text>
                        </Button>
                        <Button
                            type="submit"
                            onClick={handleConnectClick}
                            disabled={!formRef.current?.isValid}
                        >
                            <Text>settings.connect</Text>
                        </Button>
                    </ControlsWrapper>
                </form>
            </FormWrapper>
        </Wrapper>
    );
}
Example #23
Source File: ContactForm.tsx    From help-widget with MIT License 4 votes vote down vote up
ContactForm = () => {
    const config = useContext(ConfigContext);
    const service = useContext(ServiceContext);
    const router = useContext(RouterContext);
    const mounted = useIsMounted();

    const [submitting, setSubmitting] = useState(false);
    const [serverError, setServerError] = useState('');

    const [emailValue, setEmailValue] = useState('');
    const emailError = useMemo(
        () => mounted.current && (!emailValue || !(/^\S+@\S+$/.test(emailValue)))
            ? 'Email is required and must be valid' : '',
        [emailValue, submitting, mounted]);

    const [messageValue, setMessageValue] = useState('');
    const messageError = useMemo(
        () => mounted.current && (!messageValue || messageValue.length < 5)
            ? 'Text is required and must contain at least 5 characters' : '',
        [messageValue, submitting, mounted]);

    const formValid = useMemo(
        () => ![emailError, messageError].reduce((m, n) => m + n),
        [emailError, messageError]);

    useEffect(() => {
        if (!submitting) {
            return;
        }
        setServerError(''); // reset previous server error
        if (!formValid) {
            setSubmitting(false);
            return;
        }

        console.log('Sending form', { emailValue, messageValue });
        service?.sendForm({ email: emailValue, message: messageValue })
            .then(() => {
                router.setRoute('/thankyou');
            })
            .catch(() => {
                setServerError(`Something went wrong and we couldn't send your form. Please try again later.`);
            })
            .then(() => setSubmitting(false));
    }, [formValid, submitting, emailValue, messageValue, service]);

    return (
        <div>
            <p>{config.text.formSubTitle ??
                <Fragment>
                    Leave your message and we'll get back to you shortly.
                    You can also read our <RouteLink href='/faq'>FAQ</RouteLink>.</Fragment>}</p>
            <form
                onSubmit={(e) => {
                    e.preventDefault();
                    setSubmitting(true);
                }}>
                {serverError && <div className={style.error}>{serverError}</div>}
                <Field
                    name='email'
                    title='Email'
                    error={emailError}
                    render={(inputProps) => (
                        <input
                            type='text'
                            inputMode='email'
                            disabled={submitting}
                            placeholder='[email protected]'
                            autoFocus
                            onInput={(e) => setEmailValue(e.currentTarget.value)}
                            {...inputProps}
                        />)} />
                <Field
                    name='message'
                    title='Message'
                    error={messageError}
                    render={(inputProps) => (
                        <textarea
                            rows={7}
                            disabled={submitting}
                            autoComplete='disable'
                            onInput={(e) => setMessageValue(e.currentTarget.value)}
                            {...inputProps}
                        />)} />

                <div className={style.actions}>
                    <button type='submit' disabled={submitting || !formValid}>
                        {submitting ? 'Sending...' : 'Send'}
                    </button>
                </div>
            </form>
        </div >);
}
Example #24
Source File: pagination.tsx    From gridjs with MIT License 4 votes vote down vote up
renderPages() {
    if (this.props.buttonsCount <= 0) {
      return null;
    }

    // how many pagination buttons to render?
    const maxCount: number = Math.min(this.pages, this.props.buttonsCount);

    let pagePivot = Math.min(this.state.page, Math.floor(maxCount / 2));
    if (this.state.page + Math.floor(maxCount / 2) >= this.pages) {
      pagePivot = maxCount - (this.pages - this.state.page);
    }

    return (
      <Fragment>
        {this.pages > maxCount && this.state.page - pagePivot > 0 && (
          <Fragment>
            <button
              tabIndex={0}
              role="button"
              onClick={this.setPage.bind(this, 0)}
              title={this._('pagination.firstPage')}
              aria-label={this._('pagination.firstPage')}
              className={this.config.className.paginationButton}
            >
              {this._('1')}
            </button>
            <button
              tabIndex={-1}
              className={classJoin(
                className('spread'),
                this.config.className.paginationButton,
              )}
            >
              ...
            </button>
          </Fragment>
        )}

        {Array.from(Array(maxCount).keys())
          .map((i) => this.state.page + (i - pagePivot))
          .map((i) => (
            <button
              tabIndex={0}
              role="button"
              onClick={this.setPage.bind(this, i)}
              className={classJoin(
                this.state.page === i
                  ? classJoin(
                      className('currentPage'),
                      this.config.className.paginationButtonCurrent,
                    )
                  : null,
                this.config.className.paginationButton,
              )}
              title={this._('pagination.page', i + 1)}
              aria-label={this._('pagination.page', i + 1)}
            >
              {this._(`${i + 1}`)}
            </button>
          ))}

        {this.pages > maxCount && this.pages > this.state.page + pagePivot + 1 && (
          <Fragment>
            <button
              tabIndex={-1}
              className={classJoin(
                className('spread'),
                this.config.className.paginationButton,
              )}
            >
              ...
            </button>
            <button
              tabIndex={0}
              role="button"
              onClick={this.setPage.bind(this, this.pages - 1)}
              title={this._('pagination.page', this.pages)}
              aria-label={this._('pagination.page', this.pages)}
              className={this.config.className.paginationButton}
            >
              {this._(`${this.pages}`)}
            </button>
          </Fragment>
        )}
      </Fragment>
    );
  }
Example #25
Source File: markdown.tsx    From obsidian-dataview with MIT License 4 votes vote down vote up
/** Intelligently render an arbitrary literal value. */
export function RawLit({
    value,
    sourcePath,
    inline = false,
    depth = 0,
}: {
    value: Literal | undefined;
    sourcePath: string;
    inline?: boolean;
    depth?: number;
}) {
    const context = useContext(DataviewContext);

    // Short-circuit if beyond the maximum render depth.
    if (depth >= context.settings.maxRecursiveRenderDepth) return <Fragment>...</Fragment>;

    if (Values.isNull(value) || value === undefined) {
        return <Markdown content={context.settings.renderNullAs} sourcePath={sourcePath} />;
    } else if (Values.isString(value)) {
        return <Markdown content={value} sourcePath={sourcePath} />;
    } else if (Values.isNumber(value)) {
        return <Fragment>{"" + value}</Fragment>;
    } else if (Values.isBoolean(value)) {
        return <Fragment>{"" + value}</Fragment>;
    } else if (Values.isDate(value)) {
        return <Fragment>{renderMinimalDate(value, context.settings, currentLocale())}</Fragment>;
    } else if (Values.isDuration(value)) {
        return <Fragment>{renderMinimalDuration(value)}</Fragment>;
    } else if (Values.isLink(value)) {
        // Special case handling of image/video/etc embeddings to bypass the Obsidian API not working.
        if (isImageEmbed(value)) {
            let realFile = context.app.metadataCache.getFirstLinkpathDest(value.path, sourcePath);
            if (!realFile) return <Markdown content={value.markdown()} sourcePath={sourcePath} />;

            let dimensions = extractImageDimensions(value);
            let resourcePath = context.app.vault.getResourcePath(realFile);

            if (dimensions && dimensions.length == 2)
                return <img alt={value.path} src={resourcePath} width={dimensions[0]} height={dimensions[1]} />;
            else if (dimensions && dimensions.length == 1)
                return <img alt={value.path} src={resourcePath} width={dimensions[0]} />;
            else return <img alt={value.path} src={resourcePath} />;
        }

        return <Markdown content={value.markdown()} sourcePath={sourcePath} />;
    } else if (Values.isHtml(value)) {
        return <EmbedHtml element={value} />;
    } else if (Values.isFunction(value)) {
        return <Fragment>&lt;function&gt;</Fragment>;
    } else if (Values.isArray(value) || DataArray.isDataArray(value)) {
        if (!inline) {
            return (
                <ul class={"dataview dataview-ul dataview-result-list-ul"}>
                    {value.map(subvalue => (
                        <li class="dataview-result-list-li">
                            <Lit value={subvalue} sourcePath={sourcePath} inline={inline} depth={depth + 1} />
                        </li>
                    ))}
                </ul>
            );
        } else {
            if (value.length == 0) return <Fragment>&lt;Empty List&gt;</Fragment>;

            return (
                <span class="dataview dataview-result-list-span">
                    {value.map((subvalue, index) => (
                        <Fragment>
                            {index == 0 ? "" : ", "}
                            <Lit value={subvalue} sourcePath={sourcePath} inline={inline} depth={depth + 1} />
                        </Fragment>
                    ))}
                </span>
            );
        }
    } else if (Values.isObject(value)) {
        // Don't render classes in case they have recursive references; spoopy.
        if (value?.constructor?.name && value?.constructor?.name != "Object") {
            return <Fragment>&lt;{value.constructor.name}&gt;</Fragment>;
        }

        if (!inline) {
            return (
                <ul class="dataview dataview-ul dataview-result-object-ul">
                    {Object.entries(value).map(([key, value]) => (
                        <li class="dataview dataview-li dataview-result-object-li">
                            {key}: <Lit value={value} sourcePath={sourcePath} inline={inline} depth={depth + 1} />
                        </li>
                    ))}
                </ul>
            );
        } else {
            if (Object.keys(value).length == 0) return <Fragment>&lt;Empty Object&gt;</Fragment>;

            return (
                <span class="dataview dataview-result-object-span">
                    {Object.entries(value).map(([key, value], index) => (
                        <Fragment>
                            {index == 0 ? "" : ", "}
                            {key}: <Lit value={value} sourcePath={sourcePath} inline={inline} depth={depth + 1} />
                        </Fragment>
                    ))}
                </span>
            );
        }
    }

    return <Fragment>&lt;Unrecognized: {JSON.stringify(value)}&gt;</Fragment>;
}
Example #26
Source File: Field.tsx    From adyen-web with MIT License 4 votes vote down vote up
Field: FunctionalComponent<FieldProps> = props => {
    //
    const {
        children,
        className,
        classNameModifiers,
        dir,
        disabled,
        errorMessage,
        helper,
        inputWrapperModifiers,
        isCollatingErrors,
        isLoading,
        isValid,
        label,
        name,
        onBlur,
        onFieldBlur,
        onFocus,
        onFocusField,
        showValidIcon,
        useLabelElement,
        // Redeclare prop names to avoid internal clashes
        filled: propsFilled,
        focused: propsFocused
    } = props;

    const uniqueId = useRef(getUniqueId(`adyen-checkout-${name}`));

    const [focused, setFocused] = useState(false);
    const [filled, setFilled] = useState(false);

    // The means by which focussed/filled is set for securedFields
    if (propsFocused != null) setFocused(!!propsFocused);
    if (propsFilled != null) setFilled(!!propsFilled);

    // The means by which focussed/filled is set for other fields - this function is passed down to them and triggered
    const onFocusHandler = useCallback(
        (event: h.JSX.TargetedEvent<HTMLInputElement>) => {
            setFocused(true);
            onFocus?.(event);
        },
        [onFocus]
    );

    const onBlurHandler = useCallback(
        (event: h.JSX.TargetedEvent<HTMLInputElement>) => {
            setFocused(false);
            onBlur?.(event);
            // When we also need to fire a specific function when a field blurs
            onFieldBlur?.(event);
        },
        [onBlur, onFieldBlur]
    );

    const renderContent = useCallback(() => {
        return (
            <Fragment>
                {typeof label === 'string' && (
                    <span
                        className={classNames({
                            'adyen-checkout__label__text': true,
                            'adyen-checkout__label__text--error': errorMessage
                        })}
                    >
                        {label}
                    </span>
                )}

                {/*@ts-ignore - function is callable*/}
                {typeof label === 'function' && label()}

                {helper && <span className={'adyen-checkout__helper-text'}>{helper}</span>}
                <div
                    className={classNames([
                        'adyen-checkout__input-wrapper',
                        ...inputWrapperModifiers.map(m => `adyen-checkout__input-wrapper--${m}`)
                    ])}
                    dir={dir}
                >
                    {toChildArray(children).map(
                        (child: ComponentChild): ComponentChild => {
                            const childProps = {
                                isValid,
                                onFocusHandler,
                                onBlurHandler,
                                isInvalid: !!errorMessage,
                                ...(name && { uniqueId: uniqueId.current })
                            };
                            return cloneElement(child as VNode, childProps);
                        }
                    )}

                    {isLoading && (
                        <span className="adyen-checkout-input__inline-validation adyen-checkout-input__inline-validation--loading">
                            <Spinner size="small" />
                        </span>
                    )}

                    {isValid && showValidIcon !== false && (
                        <span className="adyen-checkout-input__inline-validation adyen-checkout-input__inline-validation--valid">
                            <Icon type="checkmark" />
                        </span>
                    )}

                    {errorMessage && (
                        <span className="adyen-checkout-input__inline-validation adyen-checkout-input__inline-validation--invalid">
                            <Icon type="field_error" />
                        </span>
                    )}
                </div>
                {errorMessage && typeof errorMessage === 'string' && errorMessage.length && (
                    <span
                        className={'adyen-checkout__error-text'}
                        id={`${uniqueId.current}${ARIA_ERROR_SUFFIX}`}
                        aria-hidden={isCollatingErrors ? 'true' : null}
                        aria-live={isCollatingErrors ? null : 'polite'}
                    >
                        {errorMessage}
                    </span>
                )}
            </Fragment>
        );
    }, [children, errorMessage, isLoading, isValid, label, onFocusHandler, onBlurHandler]);

    const LabelOrDiv = useCallback(({ onFocusField, focused, filled, disabled, name, uniqueId, useLabelElement, children }) => {
        const defaultWrapperProps = {
            onClick: onFocusField,
            className: classNames({
                'adyen-checkout__label': true,
                'adyen-checkout__label--focused': focused,
                'adyen-checkout__label--filled': filled,
                'adyen-checkout__label--disabled': disabled
            })
        };

        return useLabelElement ? (
            <label {...defaultWrapperProps} htmlFor={name && uniqueId}>
                {children}
            </label>
        ) : (
            <div {...defaultWrapperProps} role={'form'}>
                {children}
            </div>
        );
    }, []);

    /**
     * RENDER
     */
    return (
        <div
            className={classNames(
                'adyen-checkout__field',
                className,
                classNameModifiers.map(m => `adyen-checkout__field--${m}`),
                {
                    'adyen-checkout__field--error': errorMessage,
                    'adyen-checkout__field--valid': isValid
                }
            )}
        >
            <LabelOrDiv
                onFocusField={onFocusField}
                name={name}
                disabled={disabled}
                filled={filled}
                focused={focused}
                useLabelElement={useLabelElement}
                uniqueId={uniqueId.current}
            >
                {renderContent()}
            </LabelOrDiv>
        </div>
    );
}
Example #27
Source File: CardInput.tsx    From adyen-web with MIT License 4 votes vote down vote up
CardInput: FunctionalComponent<CardInputProps> = props => {
    const sfp = useRef(null);
    const billingAddressRef = useRef(null);
    const isValidating = useRef(false);

    const cardInputRef = useRef<CardInputRef>({});
    // Just call once
    if (!Object.keys(cardInputRef.current).length) {
        props.setComponentRef(cardInputRef.current);
    }

    const hasPanLengthRef = useRef(0);
    const isAutoJumping = useRef(false);

    const errorFieldId = 'creditCardErrors';

    const { collateErrors, moveFocus, showPanel } = props.SRConfig;

    const specifications = useMemo(() => new Specifications(props.specifications), [props.specifications]);

    // Creates access to sfp so we can call functionality on it (like handleOnAutoComplete) directly from the console. Used for testing.
    if (process.env.NODE_ENV === 'development') cardInputRef.current.sfp = sfp;

    /**
     * STATE HOOKS
     */
    const [status, setStatus] = useState('ready');

    const [errors, setErrors] = useState<CardInputErrorState>({});
    const [valid, setValid] = useState<CardInputValidState>({
        ...(props.holderNameRequired && { holderName: false })
    });
    const [data, setData] = useState<CardInputDataState>({
        ...(props.hasHolderName && { holderName: props.data.holderName ?? '' })
    });

    // An object containing a collection of all the errors that can be passed to the ErrorPanel to be read by the screenreader
    const [mergedSRErrors, setMergedSRErrors] = useState<ErrorPanelObj>(null);

    const [focusedElement, setFocusedElement] = useState('');
    const [isSfpValid, setIsSfpValid] = useState(false);
    const [expiryDatePolicy, setExpiryDatePolicy] = useState(DATE_POLICY_REQUIRED);
    const [cvcPolicy, setCvcPolicy] = useState(CVC_POLICY_REQUIRED);
    const [issuingCountryCode, setIssuingCountryCode] = useState<string>(null);

    const [dualBrandSelectElements, setDualBrandSelectElements] = useState([]);
    const [selectedBrandValue, setSelectedBrandValue] = useState('');

    const showBillingAddress = props.billingAddressMode !== AddressModeOptions.none && props.billingAddressRequired;

    const partialAddressSchema = handlePartialAddressMode(props.billingAddressMode);

    const [storePaymentMethod, setStorePaymentMethod] = useState(false);
    const [billingAddress, setBillingAddress] = useState<AddressData>(showBillingAddress ? props.data.billingAddress : null);
    const [showSocialSecurityNumber, setShowSocialSecurityNumber] = useState(false);
    const [socialSecurityNumber, setSocialSecurityNumber] = useState('');
    const [installments, setInstallments] = useState<InstallmentsObj>({ value: null });

    // re. Disable arrows for iOS: The name of the element calling for other elements to be disabled
    // - either a securedField type (like 'encryptedCardNumber') when call is coming from SF
    // or else the name of an internal, Adyen-web, element (like 'holderName')
    const [iOSFocusedField, setIOSFocusedField] = useState(null);

    /**
     * LOCAL VARS
     */
    const {
        handleChangeFor,
        triggerValidation,
        data: formData,
        valid: formValid,
        errors: formErrors,
        setSchema,
        setData: setFormData,
        setValid: setFormValid,
        setErrors: setFormErrors
    } = useForm<CardInputDataState>({
        schema: [],
        defaultData: props.data,
        formatters: cardInputFormatters,
        rules: cardInputValidationRules
    });

    const hasInstallments = !!Object.keys(props.installmentOptions).length;
    const showAmountsInInstallments = props.showInstallmentAmounts ?? true;

    const cardCountryCode: string = issuingCountryCode ?? props.countryCode;
    const isKorea = cardCountryCode === 'kr'; // If issuingCountryCode or the merchant defined countryCode is set to 'kr'
    const showKCP = props.configuration.koreanAuthenticationRequired && isKorea;

    const showBrazilianSSN: boolean =
        (showSocialSecurityNumber && props.configuration.socialSecurityNumberMode === 'auto') ||
        props.configuration.socialSecurityNumberMode === 'show';

    /**
     * HANDLERS
     */
    // SecuredField-only handler
    const handleFocus = getFocusHandler(setFocusedElement, props.onFocus, props.onBlur);

    const retrieveLayout = () => {
        return getLayout({
            props,
            showKCP,
            showBrazilianSSN,
            ...(props.billingAddressRequired && {
                countrySpecificSchemas: specifications.getAddressSchemaForCountry(billingAddress?.country),
                billingAddressRequiredFields: props.billingAddressRequiredFields
            })
        });
    };

    /**
     * re. Disabling arrow keys in iOS:
     * Only by disabling all fields in the Card PM except for the active securedField input can we force the iOS soft keyboard arrow keys to disable
     *
     * @param obj - has fieldType prop saying whether this function is being called in response to an securedFields click ('encryptedCardNumber' etc)
     * - in which case we should disable all non-SF fields
     * or,
     * due to an internal action ('webInternalElement') - in which case we can enable all non-SF fields
     */
    const handleTouchstartIOS = useCallback((obj: TouchStartEventObj) => {
        const elementType = obj.fieldType !== 'webInternalElement' ? obj.fieldType : obj.name;
        setIOSFocusedField(elementType);
    }, []);

    // Callback for ErrorPanel
    const handleErrorPanelFocus = getErrorPanelHandler(isValidating, sfp, handleFocus);

    const handleAddress = getAddressHandler(setFormData, setFormValid, setFormErrors);

    const doPanAutoJump = getAutoJumpHandler(isAutoJumping, sfp, retrieveLayout());

    const handleSecuredFieldsChange = (sfState: SFPState, eventDetails?: OnChangeEventDetails): void => {
        // Clear errors so that the screenreader will read them *all* again - without this it only reads the newly added ones
        setMergedSRErrors(null);

        /**
         * Handling auto complete value for holderName (but only if the component is using a holderName field)
         */
        if (sfState.autoCompleteName) {
            if (!props.hasHolderName) return;
            const holderNameValidationFn = getRuleByNameAndMode('holderName', 'blur');
            const acHolderName = holderNameValidationFn(sfState.autoCompleteName) ? sfState.autoCompleteName : null;
            if (acHolderName) {
                setFormData('holderName', acHolderName);
                setFormValid('holderName', true); // only if holderName is valid does this fny get called - so we know it's valid and w/o error
                setFormErrors('holderName', null);
            }
            return;
        }

        /**
         * If PAN has just become valid: decide if we can shift focus to the next field.
         *
         * We can if the config prop, autoFocus, is true AND we have a panLength value from binLookup AND one of the following scenarios is true:
         *  - If encryptedCardNumber was invalid but now is valid
         *      [scenario: shopper has typed in a number and field is now valid]
         *  - If encryptedCardNumber was valid and still is valid and we're handling an onBrand event (triggered by binLookup which has happened after the handleOnFieldValid event)
         *     [scenario: shopper has pasted in a full, valid, number]
         */
        if (
            props.autoFocus &&
            hasPanLengthRef.current > 0 &&
            ((!valid.encryptedCardNumber && sfState.valid?.encryptedCardNumber) ||
                (valid.encryptedCardNumber && sfState.valid.encryptedCardNumber && eventDetails.event === 'handleOnBrand'))
        ) {
            doPanAutoJump();
        }

        /**
         * Process SFP state
         */
        setData({ ...data, ...sfState.data });
        setErrors({ ...errors, ...sfState.errors });
        setValid({ ...valid, ...sfState.valid });

        setIsSfpValid(sfState.isSfpValid);

        // Values relating to /binLookup response
        setCvcPolicy(sfState.cvcPolicy);
        setShowSocialSecurityNumber(sfState.showSocialSecurityNumber);
        setExpiryDatePolicy(sfState.expiryDatePolicy);
    };

    // Farm the handlers for binLookup related functionality out to another 'extensions' file
    const extensions = useMemo(
        () =>
            CIExtensions(
                props,
                { sfp },
                { dualBrandSelectElements, setDualBrandSelectElements, setSelectedBrandValue, issuingCountryCode, setIssuingCountryCode },
                hasPanLengthRef
            ),
        [dualBrandSelectElements, issuingCountryCode]
    );

    /**
     * EXPOSE METHODS expected by Card.tsx
     */
    cardInputRef.current.showValidation = () => {
        // Clear errors so that the screenreader will read them *all* again
        setMergedSRErrors(null);

        // Validate SecuredFields
        sfp.current.showValidation();

        // Validate holderName & SSN & KCP (taxNumber) but *not* billingAddress
        triggerValidation(['holderName', 'socialSecurityNumber', 'taxNumber']);

        // Validate Address
        if (billingAddressRef?.current) billingAddressRef.current.showValidation();

        isValidating.current = true;
    };

    cardInputRef.current.processBinLookupResponse = (binLookupResponse: BinLookupResponse, isReset: boolean) => {
        extensions.processBinLookup(binLookupResponse, isReset);
    };

    cardInputRef.current.setStatus = setStatus;

    /**
     * EFFECT HOOKS
     */
    useEffect(() => {
        // componentDidMount - expose more methods expected by Card.tsx
        cardInputRef.current.setFocusOn = sfp.current.setFocusOn;
        cardInputRef.current.updateStyles = sfp.current.updateStyles;
        cardInputRef.current.handleUnsupportedCard = sfp.current.handleUnsupportedCard;

        // componentWillUnmount
        return () => {
            sfp.current.destroy();
        };
    }, []);

    /**
     * Handle form schema updates
     */
    useEffect(() => {
        const newSchema = [
            ...(props.hasHolderName ? ['holderName'] : []),
            ...(showBrazilianSSN ? ['socialSecurityNumber'] : []),
            ...(showKCP ? ['taxNumber'] : []),
            ...(showBillingAddress ? ['billingAddress'] : [])
        ];
        setSchema(newSchema);
    }, [props.hasHolderName, showBrazilianSSN, showKCP]);

    /**
     * Handle updates from useForm
     */
    useEffect(() => {
        // Clear errors so that the screenreader will read them *all* again
        setMergedSRErrors(null);

        setData({ ...data, holderName: formData.holderName ?? '', taxNumber: formData.taxNumber });

        setSocialSecurityNumber(formData.socialSecurityNumber);

        if (showBillingAddress) setBillingAddress({ ...formData.billingAddress });

        setValid({
            ...valid,
            holderName: props.holderNameRequired ? formValid.holderName : true,
            // Setting value to false if it's falsy keeps in line with existing, expected behaviour
            // - but there is an argument to allow 'undefined' as a value to indicate the non-presence of the field
            socialSecurityNumber: formValid.socialSecurityNumber ? formValid.socialSecurityNumber : false,
            taxNumber: formValid.taxNumber ? formValid.taxNumber : false,
            billingAddress: formValid.billingAddress ? formValid.billingAddress : false
        });

        // Check if billingAddress errors object has any properties that aren't null or undefined
        const addressHasErrors = formErrors.billingAddress
            ? Object.entries(formErrors.billingAddress).reduce((acc, [, error]) => acc || error != null, false)
            : false;

        // Errors
        setErrors({
            ...errors,
            holderName: props.holderNameRequired && !!formErrors.holderName ? formErrors.holderName : null,
            socialSecurityNumber: showBrazilianSSN && !!formErrors.socialSecurityNumber ? formErrors.socialSecurityNumber : null,
            taxNumber: showKCP && !!formErrors.taxNumber ? formErrors.taxNumber : null,
            billingAddress: showBillingAddress && addressHasErrors ? formErrors.billingAddress : null
        });
    }, [formData, formValid, formErrors]);

    /**
     * Main 'componentDidUpdate' handler
     */
    useEffect(() => {
        const holderNameValid: boolean = valid.holderName;

        const sfpValid: boolean = isSfpValid;
        const addressValid: boolean = showBillingAddress ? valid.billingAddress : true;

        const koreanAuthentication: boolean = showKCP ? !!valid.taxNumber && !!valid.encryptedPassword : true;

        const socialSecurityNumberValid: boolean = showBrazilianSSN ? !!valid.socialSecurityNumber : true;

        const isValid: boolean = sfpValid && holderNameValid && addressValid && koreanAuthentication && socialSecurityNumberValid;

        const sfStateErrorsObj = sfp.current.mapErrorsToValidationRuleResult();
        const mergedErrors = { ...errors, ...sfStateErrorsObj }; // maps sfErrors AND solves race condition problems for sfp from showValidation

        // Extract and then flatten billingAddress errors into a new object with *all* the field errors at top level
        const { billingAddress: extractedAddressErrors, ...errorsWithoutAddress } = mergedErrors;
        const errorsForPanel = { ...errorsWithoutAddress, ...extractedAddressErrors };

        const sortedMergedErrors = sortErrorsForPanel({
            errors: errorsForPanel,
            layout: retrieveLayout(),
            i18n: props.i18n,
            countrySpecificLabels: specifications.getAddressLabelsForCountry(billingAddress?.country)
        });
        setMergedSRErrors(sortedMergedErrors);

        props.onChange({
            data,
            valid,
            errors: mergedErrors,
            isValid,
            billingAddress,
            selectedBrandValue,
            storePaymentMethod,
            socialSecurityNumber,
            installments
        });
    }, [data, valid, errors, selectedBrandValue, storePaymentMethod, installments]);

    /**
     * RENDER
     */
    const FieldToRender = props.storedPaymentMethodId ? StoredCardFieldsWrapper : CardFieldsWrapper;

    return (
        <Fragment>
            <SecuredFieldsProvider
                ref={sfp}
                {...extractPropsForSFP(props)}
                styles={{ ...defaultStyles, ...props.styles }}
                koreanAuthenticationRequired={props.configuration.koreanAuthenticationRequired}
                hasKoreanFields={!!(props.configuration.koreanAuthenticationRequired && props.countryCode === 'kr')}
                onChange={handleSecuredFieldsChange}
                onBrand={props.onBrand}
                onFocus={handleFocus}
                type={props.brand}
                isCollatingErrors={collateErrors}
                onTouchstartIOS={handleTouchstartIOS}
                render={({ setRootNode, setFocusOn }, sfpState) => (
                    <div
                        ref={setRootNode}
                        className={`adyen-checkout__card-input ${styles['card-input__wrapper']} adyen-checkout__card-input--${props.fundingSource ??
                            'credit'}`}
                        role={collateErrors && 'form'}
                        aria-describedby={collateErrors ? errorFieldId : null}
                    >
                        <FieldToRender
                            // Extract exact props that we need to pass down
                            {...extractPropsForCardFields(props)}
                            // Pass on vars created in CardInput:
                            // Base (shared w. StoredCard)
                            data={data}
                            valid={valid}
                            errors={errors}
                            handleChangeFor={handleChangeFor}
                            focusedElement={focusedElement}
                            setFocusOn={setFocusOn}
                            sfpState={sfpState}
                            collateErrors={collateErrors}
                            errorFieldId={errorFieldId}
                            cvcPolicy={cvcPolicy}
                            hasInstallments={hasInstallments}
                            showAmountsInInstallments={showAmountsInInstallments}
                            handleInstallments={setInstallments}
                            // For Card
                            brandsIcons={props.brandsIcons}
                            mergedSRErrors={mergedSRErrors}
                            moveFocus={moveFocus}
                            showPanel={showPanel}
                            handleErrorPanelFocus={handleErrorPanelFocus}
                            formData={formData}
                            formErrors={formErrors}
                            formValid={formValid}
                            expiryDatePolicy={expiryDatePolicy}
                            dualBrandSelectElements={dualBrandSelectElements}
                            extensions={extensions}
                            selectedBrandValue={selectedBrandValue}
                            // For KCP
                            showKCP={showKCP}
                            // For SSN
                            showBrazilianSSN={showBrazilianSSN}
                            socialSecurityNumber={socialSecurityNumber}
                            // For Store details
                            handleOnStoreDetails={setStorePaymentMethod}
                            // For Address
                            billingAddress={billingAddress}
                            handleAddress={handleAddress}
                            billingAddressRef={billingAddressRef}
                            partialAddressSchema={partialAddressSchema}
                            //
                            iOSFocusedField={iOSFocusedField}
                        />
                    </div>
                )}
            />
            {props.showPayButton &&
                props.payButton({
                    status,
                    icon: getImage({ loadingContext: props.loadingContext, imageFolder: 'components/' })('lock')
                })}
        </Fragment>
    );
}