Skip to content

Commit

Permalink
ForwardRef support for PasswordField, FieldGroup, & FieldGroupIconBut…
Browse files Browse the repository at this point in the history
…ton (#832)

* ForwardRef support for PasswordField

* ForwardRef for FieldGroup

* ForwardRef FieldGroupIconButton
  • Loading branch information
reesscot authored Nov 22, 2021
1 parent 38e6fb7 commit f81aa59
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 46 deletions.
5 changes: 5 additions & 0 deletions .changeset/new-pumpkins-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aws-amplify/ui-react": patch
---

ForwardRef support for PasswordField, FieldGroup, & FieldGroupIconButton primitives.
36 changes: 23 additions & 13 deletions packages/react/src/primitives/FieldGroup/FieldGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import * as React from 'react';
import classNames from 'classnames';

import { Flex } from '../Flex';
import { ComponentClassNames } from '../shared/constants';
import { FieldGroupOptions, Primitive } from '../types';
import { FieldGroupOptions, PrimitiveWithForwardRef } from '../types';
import { Flex } from '../Flex';
import { View } from '../View';

export const FieldGroup: Primitive<FieldGroupOptions, typeof Flex> = ({
children,
className,
orientation = 'horizontal',
outerStartComponent,
outerEndComponent,
innerStartComponent,
innerEndComponent,
...rest
}) => {
// Don't apply field group has icon classnames unless an icon was provided
const FieldGroupPrimitive: PrimitiveWithForwardRef<
FieldGroupOptions,
typeof Flex
> = (
{
children,
className,
innerEndComponent,
innerStartComponent,
orientation = 'horizontal',
outerEndComponent,
outerStartComponent,
...rest
},
ref
) => {
// Don't apply hasInner classnames unless a component was provided
const hasInnerStartComponent = innerStartComponent != null;
const hasInnerEndComponent = innerEndComponent != null;
const fieldGroupHasInnerStartClassName = hasInnerStartComponent
Expand All @@ -34,6 +41,7 @@ export const FieldGroup: Primitive<FieldGroupOptions, typeof Flex> = ({
className
)}
data-orientation={orientation}
ref={ref}
{...rest}
>
{outerStartComponent && (
Expand Down Expand Up @@ -67,4 +75,6 @@ export const FieldGroup: Primitive<FieldGroupOptions, typeof Flex> = ({
);
};

export const FieldGroup = React.forwardRef(FieldGroupPrimitive);

FieldGroup.displayName = 'FieldGroup';
115 changes: 98 additions & 17 deletions packages/react/src/primitives/FieldGroup/__tests__/FieldGroup.test.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as React from 'react';
import { render, screen } from '@testing-library/react';

import { FieldGroup } from '../FieldGroup';
import { Text } from '../../Text';
import { Button } from '../../Button';

import { ComponentClassNames } from '../../shared';
import { FieldGroup } from '../FieldGroup';
import { Text } from '../../Text';

describe('FieldGroup component', () => {
const testId = 'fieldGroupTestId';
Expand All @@ -18,54 +18,135 @@ describe('FieldGroup component', () => {
const fieldGroup = await screen.findByTestId(testId);

expect(fieldGroup).toHaveClass('custom-class');
expect(fieldGroup).toHaveClass(ComponentClassNames.FieldGroup);
});

it('should render FieldGroup classname when outerStartComponent provided', async () => {
it('should forward ref to DOM element', async () => {
const ref = React.createRef<HTMLDivElement>();
const innerText = '<span>hello</span>';
render(
<FieldGroup
ref={ref}
testId={testId}
outerStartComponent={<Button>Click me</Button>}
>
{innerText}
</FieldGroup>
);

await screen.findByTestId(testId);
expect(ref.current.nodeName).toBe('DIV');
expect(ref.current).toHaveClass(ComponentClassNames.FieldGroup);
});

it('should not render hasInnerStart/End ClassName when inner components not provided', async () => {
render(
<FieldGroup testId={testId}>
<Text>Hello</Text>
</FieldGroup>
);

const fieldGroup = await screen.findByTestId(testId);
expect(fieldGroup).toHaveClass(ComponentClassNames.FieldGroup);
expect(fieldGroup).not.toHaveClass(
ComponentClassNames.FieldGroupHasInnerEnd
);
expect(fieldGroup).not.toHaveClass(
ComponentClassNames.FieldGroupHasInnerStart
);
});

it('should render FieldGroup classname when outerEndComponent provided', async () => {
it('should render hasInnerStart/End classnames when inner components provided', async () => {
const innerStart = 'innerStart';
const innerEnd = 'innerEnd';

render(
<FieldGroup testId={testId} outerEndComponent={<Button>Click me</Button>}>
<FieldGroup
testId={testId}
innerEndComponent={innerEnd}
innerStartComponent={innerStart}
>
<Text>Hello</Text>
</FieldGroup>
);

const fieldGroup = await screen.findByTestId(testId);
expect(fieldGroup).toHaveClass(ComponentClassNames.FieldGroup);
expect(fieldGroup).toHaveClass(ComponentClassNames.FieldGroupHasInnerStart);
expect(fieldGroup).toHaveClass(ComponentClassNames.FieldGroupHasInnerEnd);
});

const button = await screen.findByRole('button');
expect(button.innerHTML).toBe('Click me');
it('should render inner components when provided', async () => {
const innerStart = 'innerStart';
const innerEnd = 'innerEnd';

render(
<FieldGroup
testId={testId}
innerEndComponent={innerEnd}
innerStartComponent={innerStart}
>
<Text>Hello</Text>
</FieldGroup>
);

const innerStartComponent = await screen.queryByText(innerStart);
const innerEndComponent = await screen.queryByText(innerEnd);

expect(innerStartComponent).not.toBeNull();
expect(innerEndComponent).not.toBeNull();
expect(innerStartComponent).toHaveClass(
ComponentClassNames.FieldGroupInnerStart
);
expect(innerEndComponent).toHaveClass(
ComponentClassNames.FieldGroupInnerEnd
);
});

it('should render FieldGroup classname when outerEndComponent and outerStartComponent provided', async () => {
it('should render outer components when provided', async () => {
const outerStart = 'outerStart';
const outerEnd = 'outerEnd';

render(
<FieldGroup
testId={testId}
outerEndComponent={<Button>Click me</Button>}
outerStartComponent={<Button>Click me</Button>}
outerStartComponent={outerStart}
outerEndComponent={outerEnd}
>
<Text>Hello</Text>
</FieldGroup>
);

const outerStartComponent = await screen.queryByText(outerStart);
const outerEndComponent = await screen.queryByText(outerEnd);

expect(outerStartComponent).not.toBeNull();
expect(outerEndComponent).not.toBeNull();
expect(outerStartComponent).toHaveClass(
ComponentClassNames.FieldGroupOuterStart
);
expect(outerEndComponent).toHaveClass(
ComponentClassNames.FieldGroupOuterEnd
);
});

it('should set default horizontal orientation', async () => {
render(
<FieldGroup testId={testId}>
<Text>Hello</Text>
</FieldGroup>
);
const fieldGroup = await screen.findByTestId(testId);
expect(fieldGroup).toHaveClass(ComponentClassNames.FieldGroup);
expect(fieldGroup).toHaveAttribute('data-orientation', 'horizontal');
});

const buttons = await screen.findAllByRole('button');
buttons.forEach((button) => {
expect(button.innerHTML).toBe('Click me');
});
expect(buttons.length).toBe(2);
it('should set vertical orientation', async () => {
render(
<FieldGroup testId={testId} orientation="vertical">
<Text>Hello</Text>
</FieldGroup>
);
const fieldGroup = await screen.findByTestId(testId);
expect(fieldGroup).toHaveAttribute('data-orientation', 'vertical');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,24 @@ import classNames from 'classnames';
import { Button } from '../Button';
import { ComponentClassNames } from '../shared/constants';
import { FieldGroupIcon } from './FieldGroupIcon';
import { FieldGroupIconButtonProps } from '../types';
import { FieldGroupIconButtonProps, PrimitiveWithForwardRef } from '../types';

export const FieldGroupIconButton: React.FC<FieldGroupIconButtonProps> = ({
children,
className,
...rest
}) => (
const FieldGroupIconButtonPrimitive: PrimitiveWithForwardRef<
FieldGroupIconButtonProps,
'button'
> = ({ children, className, ...rest }, ref) => (
<FieldGroupIcon
as={Button}
className={classNames(ComponentClassNames.FieldGroupIconButton, className)}
ref={ref}
{...rest}
>
{children}
</FieldGroupIcon>
);

export const FieldGroupIconButton = React.forwardRef(
FieldGroupIconButtonPrimitive
);

FieldGroupIconButton.displayName = 'FieldGroupIconButton';
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from 'react';
import { render, screen } from '@testing-library/react';

import { ComponentClassNames } from '../../shared';
import { FieldGroupIconButton } from '../FieldGroupIconButton';

describe('FieldGroupIconButton component', () => {
const testId = 'fieldGroupTestId';
it('should render default and custom classname for FieldGroupIconButton', async () => {
render(<FieldGroupIconButton className="custom-class" testId={testId} />);

const fieldGroup = await screen.findByTestId(testId);

expect(fieldGroup).toHaveClass('custom-class');
expect(fieldGroup).toHaveClass(ComponentClassNames.FieldGroupIconButton);
});

it('should forward ref to DOM element', async () => {
const ref = React.createRef<HTMLButtonElement>();
render(
<FieldGroupIconButton
className="custom-class"
ref={ref}
testId={testId}
/>
);

await screen.findByRole('button');

expect(ref.current.nodeName).toBe('BUTTON');
});
});
33 changes: 23 additions & 10 deletions packages/react/src/primitives/PasswordField/PasswordField.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
import classNames from 'classnames';
import * as React from 'react';
import { ShowPasswordButton } from './ShowPasswordButton';

import { ComponentClassNames } from '../shared/constants';
import { PasswordFieldProps, PasswordFieldType, Primitive } from '../types';
import {
PasswordFieldProps,
PasswordFieldType,
PrimitiveWithForwardRef,
} from '../types';
import { ShowPasswordButton } from './ShowPasswordButton';
import { TextField } from '../TextField';

export const PasswordField: Primitive<PasswordFieldProps, 'input'> = ({
autoComplete = 'current-password',
label,
className,
hideShowPassword = false,
size,
...rest
}) => {
const PasswordFieldPrimitive: PrimitiveWithForwardRef<
PasswordFieldProps,
'input'
> = (
{
autoComplete = 'current-password',
label,
className,
hideShowPassword = false,
size,
...rest
},
ref
) => {
const [type, setType] = React.useState<PasswordFieldType>('password');

const showPasswordOnClick = React.useCallback(() => {
Expand Down Expand Up @@ -41,9 +51,12 @@ export const PasswordField: Primitive<PasswordFieldProps, 'input'> = ({
type={type}
label={label}
className={classNames(ComponentClassNames.PasswordField, className)}
ref={ref}
{...rest}
/>
);
};

export const PasswordField = React.forwardRef(PasswordFieldPrimitive);

PasswordField.displayName = 'PasswordField';
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

Expand Down Expand Up @@ -25,6 +26,14 @@ describe('PasswordField component', () => {
expect(passwordFieldWrapper).toHaveClass(ComponentClassNames.PasswordField);
});

it('should forward ref to DOM element', async () => {
const ref = React.createRef<HTMLInputElement>();
render(<PasswordField testId={testId} label="Password" ref={ref} />);

await screen.findByTestId(testId);
expect(ref.current.nodeName).toBe('INPUT');
});

it('should be password input type', async () => {
render(
<PasswordField
Expand Down

0 comments on commit f81aa59

Please sign in to comment.