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 @@ -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
5 changes: 3 additions & 2 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"@mui/utils": "^5.10.16",
"@oxygen-ui/primitives": "*",
"@oxygen-ui/react-icons": "*",
"clsx": "^1.2.1"
"clsx": "^1.2.1",
"react-world-flags": "^1.5.1"
},
"devDependencies": {
"@babel/core": "^7.20.2",
Expand All @@ -62,8 +63,8 @@
"@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
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" 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() {
return <PhoneNumberInput onChange={handlePhoneNumberInputChange} />;
}`}
/>
151 changes: 151 additions & 0 deletions packages/react/src/components/PhoneNumberInput/PhoneNumberInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* 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'>;
savindi7 marked this conversation as resolved.
Show resolved Hide resolved
/**
* Props sent to the Select component.
*
* Refer props: {@link https://mui.com/material-ui/api/select/}
*/
SelectProps?: Omit<MuiSelectProps, 'labelId' | 'id' | 'value' | 'onChange' | 'placeholder'>;
/**
* Default country selected for the dialCode.
*
* @example {code: 'US', dialCode: '+1', name: 'United States'}
*/
defaultCountry?: Country;
/**
* Callback function to be called when the dialCode or phoneNumber changes.
*/
onChange?: (dialCode: string, phoneNumber: string) => void;
/**
* 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,
defaultCountry,
label,
InputLabelProps,
OutlinedInputProps,
onChange,
placeholder,
SelectProps,
...rest
} = props;

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

const [country, setCountry] = useState<Country>(defaultCountry ?? countries[0]);
const [phoneNumber, setPhoneNumber] = useState<string>('');

const handleDialCodeChange = (event: SelectChangeEvent): void => {
setCountry(countries.find((item: Country) => item.dialCode === 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?.(country.dialCode, event.target.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={country.dialCode}
onChange={handleDialCodeChange}
renderValue={(value: string): ReactElement => (
<>
<ListItemIcon>
<Flag className="oxygen-image" alt={country.name} code={country.code} fallback={<FlagOutlined />} />
</ListItemIcon>
{value}
</>
)}
inputProps={{
className: 'oxygen-select-input-root',
}}
{...SelectProps}
>
{countries?.map((countryItem: Country) => {
const {dialCode, code, name} = countryItem;
return (
<MenuItem value={dialCode} key={code} className="oxygen-dial-code-menu-item">
<ListItemIcon>
<Flag className="oxygen-image" alt={country.name} code={code} fallback={<FlagOutlined />} />
</ListItemIcon>
<Typography>{name}</Typography>&nbsp;
<Typography variant="body2">{dialCode}</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