Skip to content

Commit

Permalink
feat(common): CHECKOUT-4223 Add address components
Browse files Browse the repository at this point in the history
  • Loading branch information
Luis Sanchez committed Aug 9, 2019
1 parent 4946631 commit fd6ad47
Show file tree
Hide file tree
Showing 27 changed files with 1,789 additions and 0 deletions.
130 changes: 130 additions & 0 deletions src/app/address/AddressSelect.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { createCheckoutService, CheckoutService } from '@bigcommerce/checkout-sdk';
import { mount, ReactWrapper } from 'enzyme';
import { noop } from 'lodash';
import React from 'react';

import { CheckoutProvider } from '../checkout';
import { getCheckout } from '../checkout/checkouts.mock';
import { getStoreConfig } from '../config/config.mock';
import { getCustomer } from '../customer/customers.mock';
import { createLocaleContext, LocaleContext, LocaleContextType } from '../locale';

import { getAddress } from './address.mock';
import StaticAddress from './staticAddress/StaticAddress';
import AddressSelect from './AddressSelect';

describe('AddressSelect Component', () => {
let checkoutService: CheckoutService;
let localeContext: LocaleContextType;
let component: ReactWrapper;

beforeEach(() => {
checkoutService = createCheckoutService();
localeContext = createLocaleContext(getStoreConfig());

jest.spyOn(checkoutService.getState().data, 'getCheckout').mockReturnValue(getCheckout());
jest.spyOn(checkoutService.getState().data, 'getConfig').mockReturnValue(getStoreConfig());
});

it('renders `Enter Address` when there is no selected address ', () => {
component = mount(
<CheckoutProvider checkoutService={ checkoutService }>
<LocaleContext.Provider value={ localeContext }>
<AddressSelect
addresses={ getCustomer().addresses }
onSelectAddress={ noop }
onUseNewAddress={ noop }
/>
</LocaleContext.Provider>
</CheckoutProvider>
);

expect(component.find('#addressToggle').text()).toEqual('Enter a new address');
});

it('renders static address when there is a selected address', () => {
component = mount(
<CheckoutProvider checkoutService={ checkoutService }>
<LocaleContext.Provider value={ localeContext }>
<AddressSelect
addresses={ getCustomer().addresses }
selectedAddress={ getAddress() }
onSelectAddress={ noop }
onUseNewAddress={ noop }
/>
</LocaleContext.Provider>
</CheckoutProvider>
);

expect(component.find(StaticAddress).prop('address')).toEqual(getAddress());
});

it('renders addresses menu when select component is clicked', () => {
component = mount(
<CheckoutProvider checkoutService={ checkoutService }>
<LocaleContext.Provider value={ localeContext }>
<AddressSelect
addresses={ getCustomer().addresses }
selectedAddress={ getAddress() }
onSelectAddress={ noop }
onUseNewAddress={ noop }
/>
</LocaleContext.Provider>
</CheckoutProvider>
);

component.find('#addressToggle').simulate('click');
const options = component.find('#addressDropdown li');

expect(options.first().text()).toEqual('Enter a new address');
expect(options.find(StaticAddress).prop('address')).toEqual(getCustomer().addresses[0]);
});

it('triggers appropriate callbacks when an item is selected', () => {
const onSelectAddress = jest.fn();
const onUseNewAddress = jest.fn();

component = mount(
<CheckoutProvider checkoutService={ checkoutService }>
<LocaleContext.Provider value={ localeContext }>
<AddressSelect
addresses={ getCustomer().addresses }
onSelectAddress={ onSelectAddress }
onUseNewAddress={ onUseNewAddress }
/>
</LocaleContext.Provider>
</CheckoutProvider>
);

component.find('#addressToggle').simulate('click');
component.find('#addressDropdown li:first-child a').simulate('click');

expect(onUseNewAddress).toHaveBeenCalled();

component.find('#addressDropdown li:last-child a').simulate('click');

expect(onSelectAddress).toHaveBeenCalledWith(getCustomer().addresses[0]);
});

it('doest not trigger onSelectAddress callback if same address is selected', () => {
const onSelectAddress = jest.fn();

component = mount(
<CheckoutProvider checkoutService={ checkoutService }>
<LocaleContext.Provider value={ localeContext }>
<AddressSelect
addresses={ getCustomer().addresses }
onSelectAddress={ onSelectAddress }
selectedAddress={ getAddress() }
onUseNewAddress={ noop }
/>
</LocaleContext.Provider>
</CheckoutProvider>
);

component.find('#addressToggle').simulate('click');
component.find('#addressDropdown li:last-child a').simulate('click');

expect(onSelectAddress).not.toHaveBeenCalled();
});
});
107 changes: 107 additions & 0 deletions src/app/address/AddressSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Address, CustomerAddress } from '@bigcommerce/checkout-sdk';
import { some } from 'lodash';
import React, { Component, FunctionComponent, ReactNode } from 'react';

import { preventDefault } from '../common/dom';
import { TranslatedString } from '../language';
import { DropdownTrigger } from '../ui/dropdown';

import StaticAddress from './staticAddress/StaticAddress';
import isEqualAddress from './util/isEqualAddress';

export interface AddressSelectProps {
addresses: CustomerAddress[];
selectedAddress?: Address;
onSelectAddress(address: Address): void;
onUseNewAddress(currentAddress?: Address): void;
}

class AddressSelect extends Component<AddressSelectProps> {
render(): ReactNode {
const {
addresses,
selectedAddress,
onUseNewAddress,
} = this.props;

return (
<div className="form-field">
<div className="dropdown--select" role="combobox">
<DropdownTrigger dropdown={
<AddressSelectMenu
addresses={ addresses }
onSelectAddress={ this.onSelectAddress }
onUseNewAddress={ () => onUseNewAddress(selectedAddress) }
selectedAddress={ selectedAddress }
/>
}>
<AddressSelectButton
addresses={ addresses }
selectedAddress={ selectedAddress }
/>
</DropdownTrigger>
</div>
</div>
);
}

private onSelectAddress: (newAddress: Address) => void = (newAddress: Address) => {
const {
onSelectAddress,
selectedAddress,
} = this.props;

if (!isEqualAddress(selectedAddress, newAddress)) {
onSelectAddress(newAddress);
}
};
}

const AddressSelectMenu: FunctionComponent<AddressSelectProps> = ({
addresses,
onSelectAddress,
onUseNewAddress,
selectedAddress,
}) => (
<ul
className="dropdown-menu instrumentSelect-dropdownMenu"
id="addressDropdown"
>
<li className="dropdown-menu-item dropdown-menu-item--select">
<a href="#" onClick={ preventDefault(() => onUseNewAddress(selectedAddress)) }>
<TranslatedString id="address.enter_address_action" />
</a>
</li>
{ addresses.map(address => (
<li
className="dropdown-menu-item dropdown-menu-item--select"
key={ address.id }
>
<a href="#" onClick={ preventDefault(() => onSelectAddress(address)) }>
<StaticAddress address={ address } />
</a>
</li>
)) }
</ul>
);

type AddressSelectButtonProps = Pick<AddressSelectProps, 'selectedAddress' | 'addresses'>;

const AddressSelectButton: FunctionComponent<AddressSelectButtonProps> = ({
selectedAddress,
addresses,
}) => (
<a
className="button dropdown-button dropdown-toggle--select"
href="#"
id="addressToggle"
onClick={ preventDefault() }
>
{ selectedAddress ?
<StaticAddress address={ selectedAddress } /> :
<TranslatedString id="address.enter_address_action" />
}
</a>
);

export default AddressSelect;
75 changes: 75 additions & 0 deletions src/app/address/CheckboxGroupFormField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { FormFieldItem } from '@bigcommerce/checkout-sdk';
import { getIn, FieldArray } from 'formik';
import { difference, kebabCase, noop } from 'lodash';
import React, { FunctionComponent, ReactNode } from 'react';

import { FormFieldContainer, FormFieldError } from '../ui/form';

import { DynamicFormFieldType } from './DynamicFormField';
import DynamicInput from './DynamicInput';
import MultiCheckboxControl from './MultiCheckboxControl';

export interface CheckboxGroupFormFieldProps {
name: string;
id: string;
label: ReactNode;
options: FormFieldItem[];
onChange?(values: string[]): void;
}

const CheckboxGroupFormField: FunctionComponent<CheckboxGroupFormFieldProps> = ({
label,
name,
id,
options,
onChange = noop,
}) => (
<FieldArray
name={ name }
render={ ({ push, remove, pop, form: { values, errors } }) => (
<FormFieldContainer hasError={ getIn(errors, name) && getIn(errors, name).length }>
{ label }
<MultiCheckboxControl
testId={ id }
onSelectedAll={ () => {
const checkedValues: string[] = getIn(values, name) || [];
difference(options.map(({ value }) => value), checkedValues)
.forEach(val => push(val));

onChange(getIn(values, name));
}}
onSelectedNone={ () => {
const checkedValues: string[] = getIn(values, name) || [];
checkedValues.forEach(() => pop());
onChange(getIn(values, name));
}}
/>
<DynamicInput
name={ name }
value={ getIn(values, name) || [] }
onChange={e => {
const checkedValues: string[] = getIn(values, name) || [];
const { value, checked } = e.target;

if (checked) {
push(value);
} else {
remove(checkedValues.indexOf(value));
}

onChange(getIn(values, name));
} }
fieldType={ DynamicFormFieldType.checkbox }
options={ options }
id={ id }
/>
<FormFieldError
name={ name }
testId={ `${kebabCase(name)}-field-error-message` }
/>
</FormFieldContainer>
)}
/>
);

export default CheckboxGroupFormField;
Loading

0 comments on commit fd6ad47

Please sign in to comment.