Skip to content

Commit

Permalink
feat: V0 Attachment compact component (#31634)
Browse files Browse the repository at this point in the history
Co-authored-by: Juraj Kapsiar <jukapsia@microsoft.com>
  • Loading branch information
jurokapsiar and Juraj Kapsiar committed Jun 24, 2024
1 parent a8eba6a commit ff2f5eb
Show file tree
Hide file tree
Showing 20 changed files with 780 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat(v0-migration): Attachment compat component",
"packageName": "@fluentui/react-migration-v0-v9",
"email": "jukapsia@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

/// <reference types="react" />

import { ButtonProps } from '@fluentui/react-components';
import { ComponentProps } from '@fluentui/react-components';
import type { ComponentProps as ComponentProps_2 } from '@fluentui/react-utilities';
import type { ComponentState } from '@fluentui/react-utilities';
Expand All @@ -22,6 +23,79 @@ import { Slot as Slot_2 } from '@fluentui/react-utilities';
import type { SlotClassNames } from '@fluentui/react-utilities';
import { SlotRenderFunction } from '@fluentui/react-utilities';

// @public (undocumented)
export const Attachment: React_2.ForwardRefExoticComponent<AttachmentProps & React_2.RefAttributes<HTMLDivElement>>;

// @public (undocumented)
export const AttachmentAction: React_2.ForwardRefExoticComponent<ButtonProps & React_2.RefAttributes<HTMLButtonElement>>;

// @public (undocumented)
export const attachmentActionClassName = "fui-AttachmentAction";

// @public (undocumented)
export type AttachmentActionProps = ButtonProps;

// @public (undocumented)
export const AttachmentBody: React_2.FC<AttachmentBodyProps>;

// @public (undocumented)
export const attachmentBodyClassName = "fui-AttachmentBody";

// @public (undocumented)
export interface AttachmentBodyProps extends React_2.HTMLAttributes<HTMLDivElement> {
}

// @public (undocumented)
export const attachmentClassName = "fui-Attachment";

// @public (undocumented)
export const AttachmentDescription: React_2.FC<AttachmentDescriptionProps>;

// @public (undocumented)
export const attachmentDescriptionClassName = "fui-AttachmentDescription";

// @public (undocumented)
export interface AttachmentDescriptionProps extends React_2.HTMLAttributes<HTMLSpanElement> {
}

// @public (undocumented)
export const AttachmentHeader: React_2.FC<AttachmentHeaderProps>;

// @public (undocumented)
export const attachmentHeaderClassName = "fui-AttachmentHeader";

// @public (undocumented)
export interface AttachmentHeaderProps extends React_2.HTMLAttributes<HTMLSpanElement> {
}

// @public (undocumented)
export const AttachmentIcon: React_2.FC<AttachmentIconProps>;

// @public (undocumented)
export const attachmentIconClassName = "fui-AttachmentIcon";

// @public (undocumented)
export interface AttachmentIconProps extends React_2.HTMLAttributes<HTMLSpanElement> {
}

// @public (undocumented)
export const attachmentProgressBarClassName: string;

// @public (undocumented)
export const attachmentProgressContainerClassName: string;

// @public (undocumented)
export interface AttachmentProps extends React_2.HTMLAttributes<HTMLElement> {
// (undocumented)
actionable?: boolean;
// (undocumented)
disabled?: boolean;
// (undocumented)
onClick?: (event: React_2.MouseEvent<HTMLDivElement, MouseEvent>) => void;
// (undocumented)
progress?: string | number;
}

// @public (undocumented)
export const Flex: React_2.ForwardRefExoticComponent<React_2.HTMLAttributes<HTMLElement> & FlexProps & React_2.RefAttributes<HTMLDivElement>>;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
createCustomFocusIndicatorStyle,
makeResetStyles,
makeStyles,
shorthands,
tokens,
} from '@fluentui/react-components';
import { attachmentActionClassName } from './AttachmentAction';
import { attachmentIconClassName } from './AttachmentIcon';

export const useAttachmentBaseStyles = makeResetStyles({
...createCustomFocusIndicatorStyle(
{
outline: `${tokens.strokeWidthThick} solid ${tokens.colorStrokeFocus2}`,
borderRadius: tokens.borderRadiusMedium,
backgroundColor: undefined,
color: undefined,
[`& .${attachmentActionClassName}`]: {
color: undefined,
},

[`& .${attachmentIconClassName}`]: {
color: undefined,
},
},
{ selector: 'focus' },
),
position: 'relative',
display: 'inline-flex',
alignItems: 'center',
width: '100%',
maxWidth: '424px',
minHeight: '32px',
...shorthands.padding('7px', '3px', '7px', '11px'),
marginBottom: '2px',
marginRight: '2px',
backgroundColor: tokens.colorNeutralBackground6,
color: tokens.colorNeutralForeground1,
boxShadow: `0 .2rem .4rem -.075rem ${tokens.colorNeutralShadowAmbient}`,
...shorthands.border('1px', 'solid', tokens.colorNeutralStroke3),
borderRadius: '4px',
});

export const useAttachmentStyles = makeStyles({
actionable: {
cursor: 'pointer',
':hover': {
backgroundColor: tokens.colorNeutralBackground4Hover,
},
},
progressContainer: {
borderBottomLeftRadius: '4px',
borderBottomRightRadius: '4px',
bottom: 0,
height: '4px',
left: 0,
overflow: 'hidden',
position: 'absolute',
right: 0,
},
progressBar: {
backgroundColor: tokens.colorPaletteLightGreenBackground3,
height: '100%',
maxWidth: '100%',
transition: 'width 0.2s',
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import '@testing-library/jest-dom';
import { isConformant } from '@fluentui/react-conformance';
import * as React from 'react';
import { render, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { Attachment } from './Attachment';

describe('Attachment', () => {
isConformant({
Component: Attachment,
componentPath: module!.filename.replace('.test', ''),
displayName: 'Attachment',
disabledTests: ['has-docblock', 'has-top-level-file', 'component-has-static-classnames-object'],
});

it('renders a default state', () => {
const { getByText } = render(<Attachment>Actionable</Attachment>);
const textElement = getByText('Actionable');
expect(textElement.nodeName).toBe('DIV');
});

it('handles onClick', () => {
const handleClick = jest.fn();
const { getByText } = render(
<Attachment actionable onClick={handleClick}>
Click me
</Attachment>,
);
fireEvent.click(getByText('Click me'));
expect(handleClick).toHaveBeenCalled();
});

it('handles Enter', () => {
const handleClick = jest.fn();
const { getByText } = render(
<Attachment actionable onClick={handleClick}>
Click me
</Attachment>,
);
userEvent.type(getByText('Click me'), '{enter}');
expect(handleClick).toHaveBeenCalled();
});

it('renders actionable', () => {
const { getByText } = render(<Attachment actionable={true}>Actionable</Attachment>);
const actionableElement = getByText('Actionable');
expect(actionableElement).toHaveAttribute('tabIndex', '0');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { mergeClasses } from '@fluentui/react-components';
import { useARIAButtonProps } from '@fluentui/react-aria';
import * as React from 'react';

import { useAttachmentBaseStyles, useAttachmentStyles } from './Attachment.styles';

export const attachmentClassName = 'fui-Attachment';
export const attachmentProgressContainerClassName = `${attachmentClassName}__progress-container`;
export const attachmentProgressBarClassName = `${attachmentClassName}__progress`;

export interface AttachmentProps extends React.HTMLAttributes<HTMLElement> {
actionable?: boolean;
disabled?: boolean;
progress?: string | number;
onClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
}

export const Attachment = React.forwardRef<HTMLDivElement, AttachmentProps>((props, ref) => {
const { actionable, className, children, disabled, onClick, progress, onKeyDown, onKeyUp, ...rest } = props;
const attachmentBaseClass = useAttachmentBaseStyles();
const classes = useAttachmentStyles();

const buttonProps = useARIAButtonProps('div', {
disabled,
onClick,
onKeyDown: onKeyDown as React.KeyboardEventHandler<HTMLLIElement & HTMLDivElement>,
onKeyUp: onKeyUp as React.KeyboardEventHandler<HTMLLIElement & HTMLDivElement>,
});

return (
<div
ref={ref}
className={mergeClasses(attachmentClassName, attachmentBaseClass, actionable && classes.actionable, className)}
{...(actionable && {
'data-is-focusable': true,
...buttonProps,
})}
{...rest}
>
{children}
{!isNaN(Number(progress)) && (
<div className={mergeClasses(attachmentProgressContainerClassName, classes.progressContainer)}>
<div
className={mergeClasses(classes.progressBar, attachmentProgressBarClassName)}
style={{ width: `${progress}%` }}
/>
</div>
)}
</div>
);
});

Attachment.displayName = 'Attachment';
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { makeStyles } from '@fluentui/react-components';

export const useAttachmentActionStyles = makeStyles({
root: {
height: '32px',
maxWidth: '280px',
minWidth: '32px',
display: 'inline-flex',
justifyContent: 'center',
alignItems: 'center',
position: 'relative',
verticalAlign: 'middle',
cursor: 'pointer',
},
disabled: {
cursor: 'default',
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import '@testing-library/jest-dom';
import * as React from 'react';
import { render, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { AttachmentAction } from './AttachmentAction';
import { Attachment } from './Attachment';

describe('AttachmentAction', () => {
it('renders a default state', () => {
const { getByText } = render(<AttachmentAction>Action</AttachmentAction>);
const textElement = getByText('Action');
expect(textElement.nodeName).toBe('BUTTON');
});

it('handles onClick', () => {
const handleClick = jest.fn();
const { getByText } = render(<AttachmentAction onClick={handleClick}>Click me</AttachmentAction>);
fireEvent.click(getByText('Click me'));
expect(handleClick).toHaveBeenCalled();
});

it('handles Enter', () => {
const handleClick = jest.fn();
const { getByText } = render(<AttachmentAction onClick={handleClick}>Click me</AttachmentAction>);
userEvent.type(getByText('Click me'), '{enter}');
expect(handleClick).toHaveBeenCalled();
});

it('handles Enter when in Attachment', () => {
const handleClick = jest.fn();
const handleAttachmentClick = jest.fn();
const { getByText } = render(
<Attachment actionable onClick={handleAttachmentClick}>
<AttachmentAction onClick={handleClick}>Click me</AttachmentAction>
</Attachment>,
);
userEvent.type(getByText('Click me'), '{enter}');
expect(handleClick).toHaveBeenCalled();
expect(handleAttachmentClick).not.toHaveBeenCalled();
});

it('handles onKeyDown', () => {
const handleKeyDown = jest.fn();
const { getByText } = render(<AttachmentAction onKeyDown={handleKeyDown}>Press key</AttachmentAction>);
fireEvent.keyDown(getByText('Press key'), { key: 'Enter' });
expect(handleKeyDown).toHaveBeenCalled();
});

it('handles onKeyUp', () => {
const handleKeyUp = jest.fn();
const { getByText } = render(<AttachmentAction onKeyUp={handleKeyUp}>Release key</AttachmentAction>);
fireEvent.keyUp(getByText('Release key'), { key: 'Enter' });
expect(handleKeyUp).toHaveBeenCalled();
});

it('renders disabled', () => {
const { getByText } = render(<AttachmentAction disabled={true}>Disabled</AttachmentAction>);
const disabledElement = getByText('Disabled');
expect(disabledElement).toBeDisabled();
});
});
Loading

0 comments on commit ff2f5eb

Please sign in to comment.