Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react): add PhoneNumberInput component #64

Merged
merged 11 commits into from
Mar 16, 2023
4 changes: 4 additions & 0 deletions packages/react/.storybook/story-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export type Stories =
| 'UserDropdownMenu'
| 'Navbar'
| 'OutlinedInput'
| 'PhoneNumberInput'
| 'SignIn'
| 'Stepper'
| 'TextField'
Expand Down Expand Up @@ -242,6 +243,9 @@ const StoryConfig: StorybookConfig = {
ListItemText: {
hierarchy: `${StorybookCategories.DataDisplay}/List Item Text`,
},
PhoneNumberInput: {
hierarchy: `${StorybookCategories.Inputs}/Phone Number Input`,
},
SignIn: {
hierarchy: `${StorybookCategories.Patterns}/Sign In`,
},
Expand Down
20 changes: 5 additions & 15 deletions packages/react/src/components/Image/Image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,21 @@
* under the License.
*/

import {Box, BoxProps} from '@mui/material';
import clsx from 'clsx';
import {FC, ReactElement} from 'react';
import {FC, ImgHTMLAttributes, ReactElement} from 'react';
import {WithWrapperProps} from '../../models';
import {composeComponentDisplayName} from '../../utils';

export interface ImageProps extends BoxProps {
/**
* Alternative text for the image.
*/
alt: string;
/**
* Source of the image.
*/
src: string;
}
export type ImageProps = ImgHTMLAttributes<HTMLImageElement>;

const COMPONENT_NAME: string = 'Image';

const Image: FC<ImageProps> & WithWrapperProps = (props: BoxProps): ReactElement => {
const {className, ...rest} = props;
const Image: FC<ImageProps> & WithWrapperProps = (props: ImageProps): ReactElement => {
const {className, alt, ...rest} = props;

const classes: string = clsx('oxygen-image', className);

return <Box className={classes} component="img" {...rest} />;
return <img className={classes} alt={alt} {...rest} />;
};

Image.displayName = composeComponentDisplayName(COMPONENT_NAME);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {ArgsTable, Source, Story, Canvas, Meta} from '@storybook/addon-docs';
import dedent from 'ts-dedent';
import StoryConfig from '../../../.storybook/story-config.ts';
import PhoneNumberInput from './PhoneNumberInput.tsx';

export const meta = {
component: PhoneNumberInput,
title: StoryConfig.PhoneNumberInput.hierarchy,
};

<Meta title={meta.title} component={meta.component} />

export const Template = args => <PhoneNumberInput {...args} />;

# Phone Number Input

- [Overview](#overview)
- [Props](#props)
- [Usage](#usage)

## Overview

Use the `PhoneNumberInput` component to collect phone numbers from users including the country dial code.

<Canvas>
<Story name="Overview" args={{label: "Mobile", placeholder: "Enter your Mobile"}}>
{Template.bind({})}
</Story>
</Canvas>

## Props

<ArgsTable story="Overview" />

## Usage

Import and use the `PhoneNumberInput` component in your components as follows.

<Source
language="jsx"
dark
format
code={dedent`
import PhoneNumberInput from '@oxygen-ui/react/PhoneNumberInput';\n
function Demo() {
return <PhoneNumberInput onChange={handlePhoneNumberInputChange} />;
}`}
/>
121 changes: 121 additions & 0 deletions packages/react/src/components/PhoneNumberInput/PhoneNumberInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). All Rights Reserved.
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import Select, {SelectChangeEvent, SelectProps as MuiSelectProps} from '@mui/material/Select';
import clsx from 'clsx';
import {ChangeEvent, forwardRef, ForwardRefExoticComponent, MutableRefObject, ReactElement, useState} from 'react';
import {countries, Country} from './constants';
import {WithWrapperProps} from '../../models';
import {composeComponentDisplayName} from '../../utils';
import Box, {BoxProps} from '../Box';
import Image from '../Image';
import InputLabel from '../InputLabel';
import ListItemIcon from '../ListItemIcon';
import MenuItem from '../MenuItem';
import './phone-number-input.scss';
import OutlinedInput, {OutlinedInputProps as MuiOutlinedInputProps} from '../OutlinedInput';

export interface PhoneNumberInputProps extends BoxProps {
OutlinedInputProps?: Omit<MuiOutlinedInputProps, 'id' | 'label' | 'placeholder' | 'type'>;
SelectProps?: Omit<MuiSelectProps, 'labelId' | 'id' | 'value' | 'onChange' | 'placeholder'>;
defaultCountryCode?: string;
onChange?: (value: string) => void;
placeholder?: string;
}

const COMPONENT_NAME: string = 'PhoneNumberInput';

const PhoneNumberInput: ForwardRefExoticComponent<PhoneNumberInputProps> & WithWrapperProps = forwardRef(
(props: PhoneNumberInputProps, ref: MutableRefObject<HTMLDivElement>): ReactElement => {
const {
className,
defaultCountryCode,
label,
InputLabelProps,
OutlinedInputProps,
onChange,
placeholder,
SelectProps,
...rest
} = props;

const classes: string = clsx('oxygen-phone-number-input', className);

const [countryCode, setCountryCode] = useState<string>(defaultCountryCode ?? countries[0].dial_code);
const [phoneNumber, setPhoneNumber] = useState('');

const handleCountryCodeChange = (event: SelectChangeEvent): void => {
setCountryCode(event.target.value);
onChange(`${event.target.value}${phoneNumber}`);
};

const handlePhoneNumberChange = (event: ChangeEvent<HTMLInputElement>): void => {
setPhoneNumber(event.target.value);
savindi7 marked this conversation as resolved.
Show resolved Hide resolved
onChange(`${countryCode}${event.target.value}`);
};

return (
<Box className={classes} {...rest} ref={ref}>
<InputLabel htmlFor="phone-number-input" id="phone-number-label">
{label}
</InputLabel>
<Box className="oxygen-select-input">
<Select
labelId="phone-number-label"
id="phone-number-select"
value={countryCode}
onChange={handleCountryCodeChange}
className="oxygen-select"
inputProps={{
className: 'oxygen-select-input-root',
}}
{...SelectProps}
>
{countries?.map((country: Country) => (
<MenuItem value={country.dial_code} className="oxygen-dial-code-menu-item">
<ListItemIcon>
<Image
loading="lazy"
src={`https://flagcdn.com/${country.code.toLowerCase()}.svg`}
srcSet={`https://flagcdn.com/${country.code.toLowerCase()}.svg 2x`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a CDN in a library does not seem right to me. Can we try using flag emojis?
https://dev.to/jorik/country-code-to-flag-emoji-a21

Copy link
Member

@brionmario brionmario Mar 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Emojis might have some issues in Windows. Maybe we can try a well maintained SVG flag library.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added react-world-flags package

alt={country.name}
/>
</ListItemIcon>
{country.dial_code}
</MenuItem>
))}
</Select>
<OutlinedInput
id="phone-number-input"
placeholder={placeholder}
type="tel"
value={phoneNumber}
onChange={handlePhoneNumberChange}
{...OutlinedInputProps}
/>
</Box>
</Box>
);
},
) as ForwardRefExoticComponent<PhoneNumberInputProps> & WithWrapperProps;

PhoneNumberInput.displayName = composeComponentDisplayName(COMPONENT_NAME);
PhoneNumberInput.muiName = COMPONENT_NAME;
PhoneNumberInput.defaultProps = {};

export default PhoneNumberInput;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). All Rights Reserved.
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import {render} from '@unit-testing';
import PhoneNumberInput from '../PhoneNumberInput';

describe('TextField', () => {
it('should render successfully', () => {
const {baseElement} = render(<PhoneNumberInput />);
expect(baseElement).toBeTruthy();
});

it('should match the snapshot', () => {
const {baseElement} = render(<PhoneNumberInput />);
expect(baseElement).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`TextField should match the snapshot 1`] = `
<body>
<div>
<div
class="oxygen-box oxygen-phone-number-input MuiBox-root css-0"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-animated MuiFormLabel-colorPrimary MuiInputLabel-root MuiInputLabel-animated oxygen-input-label css-f3834g-MuiFormLabel-root-MuiInputLabel-root"
for="phone-number-input"
id="phone-number-label"
/>
<div
class="oxygen-box oxygen-select-input MuiBox-root css-0"
>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-colorPrimary oxygen-select css-saqlya-MuiInputBase-root-MuiOutlinedInput-root-MuiSelect-root"
>
<div
aria-expanded="false"
aria-haspopup="listbox"
aria-labelledby="phone-number-label phone-number-select"
class="MuiSelect-select MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input oxygen-select-input-root css-1d3lhbp-MuiSelect-select-MuiInputBase-input-MuiOutlinedInput-input"
id="phone-number-select"
role="button"
tabindex="0"
>
<div
class="MuiListItemIcon-root oxygen-list-item-icon css-1djcs2d-MuiListItemIcon-root"
>
<img
alt="Afghanistan"
class="oxygen-image"
loading="lazy"
src="https://flagcdn.com/af.svg"
srcset="https://flagcdn.com/af.svg 2x"
/>
</div>
+93
</div>
<input
aria-hidden="true"
class="MuiSelect-nativeInput css-yf8vq0-MuiSelect-nativeInput"
tabindex="-1"
value="+93"
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium MuiSelect-icon MuiSelect-iconOutlined css-j9wse9-MuiSvgIcon-root-MuiSelect-icon"
data-testid="ArrowDropDownIcon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
<fieldset
aria-hidden="true"
class="MuiOutlinedInput-notchedOutline css-116asjz-MuiOutlinedInput-notchedOutline"
>
<legend
class="css-ihdtdm"
>
<span
class="notranslate"
>
</span>
</legend>
</fieldset>
</div>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-colorPrimary oxygen-outlined-input css-9s98je-MuiInputBase-root-MuiOutlinedInput-root"
>
<input
class="MuiInputBase-input MuiOutlinedInput-input css-d9o7zs-MuiInputBase-input-MuiOutlinedInput-input"
id="phone-number-input"
type="tel"
value=""
/>
<fieldset
aria-hidden="true"
class="MuiOutlinedInput-notchedOutline css-116asjz-MuiOutlinedInput-notchedOutline"
>
<legend
class="css-ihdtdm"
>
<span
class="notranslate"
>
</span>
</legend>
</fieldset>
</div>
</div>
</div>
</div>
</body>
`;
Loading