Skip to content

Commit

Permalink
Merge pull request #64 from savindi7/feat-phone-number-input
Browse files Browse the repository at this point in the history
feat(react): add `PhoneNumberInput` component
  • Loading branch information
savindi7 authored Mar 16, 2023
2 parents 29f40b0 + a37c2bf commit c82df6e
Show file tree
Hide file tree
Showing 12 changed files with 2,184 additions and 1,002 deletions.
4 changes: 4 additions & 0 deletions packages/react/.storybook/story-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export type Stories =
| 'UserDropdownMenu'
| 'Navbar'
| 'OutlinedInput'
| 'PhoneNumberInput'
| 'Select'
| 'SignIn'
| 'Stepper'
Expand Down Expand Up @@ -250,6 +251,9 @@ const StoryConfig: StorybookConfig = {
ListItemText: {
hierarchy: `${StorybookCategories.DataDisplay}/List Item Text`,
},
PhoneNumberInput: {
hierarchy: `${StorybookCategories.Inputs}/Phone Number Input`,
},
Select: {
hierarchy: `${StorybookCategories.Inputs}/Select`,
},
Expand Down
7 changes: 4 additions & 3 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@
"@mui/utils": "^5.10.16",
"@oxygen-ui/primitives": "*",
"@oxygen-ui/react-icons": "*",
"clsx": "^1.2.1"
"@storybook/addon-essentials": "^6.5.16",
"clsx": "^1.2.1",
"react-world-flags": "^1.5.1"
},
"devDependencies": {
"@babel/core": "^7.20.2",
Expand All @@ -57,13 +59,12 @@
"@rollup/plugin-typescript": "^10.0.1",
"@storybook/addon-a11y": "^6.5.13",
"@storybook/addon-actions": "^6.5.13",
"@storybook/addon-essentials": "^6.5.13",
"@storybook/addon-interactions": "^6.5.13",
"@storybook/addon-links": "^6.5.13",
"@storybook/builder-webpack4": "^6.5.13",
"@storybook/builder-webpack5": "^6.5.13",
"@storybook/manager-webpack4": "^6.5.13",
"@storybook/client-api": "^6.5.13",
"@storybook/manager-webpack4": "^6.5.13",
"@storybook/manager-webpack5": "^6.5.13",
"@storybook/preset-scss": "^1.0.3",
"@storybook/react": "^6.5.13",
Expand Down
24 changes: 9 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,25 @@
* 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;
/**
* TODO: Refer improvement issue if this Image component is required.
* @see {@link https://github.com/wso2/oxygen-ui/issues/65}
*/
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,60 @@
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's dial code.

<Canvas>
<Story name="Overview" argTypes={{ onChange: { action: 'Phone Number' } }} 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() {
const [dialCode, setDialCode] = useState('');
const [phoneNumber, setPhoneNumber] = useState('');\n
const handlePhoneNumberInputChange = (dialCode, phoneNumber) => {
setDialCode(dialCode);
setPhoneNumber(phoneNumber);
};\n
return (
<PhoneNumberInput
dialCodeValue={dialCode}
phoneNumberValue={phoneNumber}
onChange={handlePhoneNumberInputChange}
/>
);
}`}
/>
170 changes: 170 additions & 0 deletions packages/react/src/components/PhoneNumberInput/PhoneNumberInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* 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 {FlagOutlined} from '@mui/icons-material';
import Select, {SelectChangeEvent, SelectProps as MuiSelectProps} from '@mui/material/Select';
import clsx from 'clsx';
import {ChangeEvent, forwardRef, ForwardRefExoticComponent, MutableRefObject, ReactElement, useState} from 'react';
import Flag from 'react-world-flags';
import {countries, Country} from './constants';
import {WithWrapperProps} from '../../models';
import {composeComponentDisplayName} from '../../utils';
import Box, {BoxProps} from '../Box';
import InputLabel from '../InputLabel';
import ListItemIcon from '../ListItemIcon';
import MenuItem from '../MenuItem';
import './phone-number-input.scss';
import OutlinedInput, {OutlinedInputProps as MuiOutlinedInputProps} from '../OutlinedInput';
import Typography from '../Typography';

export interface PhoneNumberInputProps extends BoxProps {
/**
* Props sent to the OutlinedInput component.
*
* Refer props: {@link https://mui.com/material-ui/api/outlined-input/}
*/
OutlinedInputProps?: Omit<MuiOutlinedInputProps, 'id' | 'label' | 'placeholder' | 'value' | 'type'>;
/**
* Props sent to the Select component.
*
* Refer props: {@link https://mui.com/material-ui/api/select/}
*/
SelectProps?: Omit<MuiSelectProps, 'labelId' | 'id' | 'value' | 'onChange' | 'placeholder'>;
/**
* Dial code state value.
*
* @example '+94'
*/
dialCodeValue?: string;
/**
* Callback function to be called when the dialCode or phoneNumber changes.
*/
onChange?: (dialCode: string, phoneNumber: string) => void;
/**
* Phone number state value.
*
* @example '787878787'
*/
phoneNumberValue?: string;
/**
* Placeholder text for the phone number input.
*/
placeholder?: string;
}

const COMPONENT_NAME: string = 'PhoneNumberInput';

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

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

const [dialCode, setDialCode] = useState<string>(dialCodeValue ?? countries[0].dialCode);

const [phoneNumber, setPhoneNumber] = useState<string>(phoneNumberValue ?? '');

const handleDialCodeChange = (event: SelectChangeEvent): void => {
setDialCode(event.target.value);
onChange?.(event.target.value, phoneNumber);
};

const handlePhoneNumberChange = (event: ChangeEvent<HTMLInputElement>): void => {
setPhoneNumber(event.target.value);
onChange?.(dialCode, event.target.value);
};

const renderValue = (value: string): ReactElement => {
const selectedCountry: Country = countries.find((item: Country) => item.dialCode === dialCode);

return (
<>
<ListItemIcon>
<Flag
className="oxygen-image"
alt={selectedCountry.name}
code={selectedCountry.code}
fallback={<FlagOutlined />}
/>
</ListItemIcon>
{value}
</>
);
};

return (
<Box className={classes} ref={ref} {...rest}>
<InputLabel htmlFor="phone-number-input" id="phone-number-label">
{label}
</InputLabel>
<Box className="oxygen-select-input">
<Select
className="oxygen-select"
labelId="phone-number-label"
id="phone-number-select"
value={dialCode}
onChange={handleDialCodeChange}
renderValue={renderValue}
inputProps={{
className: 'oxygen-select-input-root',
}}
{...SelectProps}
>
{countries?.map((countryItem: Country) => {
const {dialCode: phoneCode, code, name} = countryItem;
return (
<MenuItem value={phoneCode} key={code} className="oxygen-dial-code-menu-item">
<ListItemIcon>
<Flag className="oxygen-image" alt={name} code={code} fallback={<FlagOutlined />} />
</ListItemIcon>
<Typography>{name}</Typography>&nbsp;
<Typography variant="body2">{phoneCode}</Typography>
</MenuItem>
);
})}
</Select>
<OutlinedInput
id="phone-number-input"
type="tel"
placeholder={placeholder}
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();
});
});

Large diffs are not rendered by default.

Loading

0 comments on commit c82df6e

Please sign in to comment.