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: V0 Attachment compact component #31634

Merged
merged 9 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
@@ -0,0 +1,61 @@
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,42 @@
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,70 @@
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 handleClick = React.useCallback(e => {
if (disabled) {
e.preventDefault();
return;
}
if (onClick) {
onClick(e);
}
}, [onClick, disabled]);

const buttonProps = useARIAButtonProps('div', {
onClick: handleClick,
jurokapsiar marked this conversation as resolved.
Show resolved Hide resolved
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
)}
onClick={handleClick}
jurokapsiar marked this conversation as resolved.
Show resolved Hide resolved
{...actionable && {
"data-is-focusable": true,
jurokapsiar marked this conversation as resolved.
Show resolved Hide resolved
...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,58 @@
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();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Button, ButtonProps, mergeClasses } from '@fluentui/react-components';
import * as React from 'react';
import { useAttachmentActionStyles } from './AttachmentAction.styles';

export type AttachmentActionProps = ButtonProps;

export const attachmentActionClassName = 'fui-AttachmentAction';

export const AttachmentAction = React.forwardRef<HTMLButtonElement, AttachmentActionProps>((props, ref) => {
const { className, disabled, disabledFocusable, children, onClick, onKeyUp, onKeyDown, ...rest } = props;
const classes = useAttachmentActionStyles();

const handleClick = React.useCallback(e => {
e.stopPropagation();
e.preventDefault();
onClick?.(e);
}, [onClick]);

const handleKeyUp = React.useCallback(e => {
if (e.key === 'Enter' || e.key === ' ') {
e.stopPropagation();
}
onKeyUp?.(e);
}, [onKeyUp]);

const handleKeyDown = React.useCallback(e => {
if (e.key === 'Enter' || e.key === ' ') {
e.stopPropagation();
}
onKeyDown?.(e);
}, [onKeyDown]);
jurokapsiar marked this conversation as resolved.
Show resolved Hide resolved

return (
<Button
ref={ref}
className={mergeClasses(
attachmentActionClassName,
classes.root,
(disabled || disabledFocusable) && classes.disabled,
className
)}
appearance='transparent'
disabled={disabled}
disabledFocusable={disabledFocusable}
onClick={handleClick}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
{...rest}
>
{children}
</Button>
);
});

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

export const useAttachmentBodyStyles = makeStyles({
root: {
flex: "1 1 0"
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { mergeClasses } from '@fluentui/react-components';
import * as React from 'react';
import { useAttachmentBodyStyles } from './AttachmentBody.styles';

export interface AttachmentBodyProps extends React.HTMLAttributes<HTMLDivElement> {}

export const attachmentBodyClassName = 'fui-AttachmentBody';

export const AttachmentBody: React.FC<AttachmentBodyProps> = React.forwardRef<HTMLDivElement, AttachmentBodyProps>(
(props, ref) => {
const { className, children, ...rest } = props;
const classes = useAttachmentBodyStyles();

return (
<div ref={ref} className={mergeClasses(attachmentBodyClassName, classes.root, className)} {...rest}>
{children}
</div>
);
},
);

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

export const useAttachmentDescriptionStyles = makeStyles({
root: {
display: 'block',
fontSize: tokens.fontSizeBase200,
fontWeight: tokens.fontWeightRegular,
lineHeight: 1,
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { mergeClasses } from '@fluentui/react-components';
import * as React from 'react';
import { useAttachmentDescriptionStyles } from './AttachmentDescription.styles';

export interface AttachmentDescriptionProps extends React.HTMLAttributes<HTMLSpanElement> {}

export const attachmentDescriptionClassName = 'fui-AttachmentDescription';

export const AttachmentDescription: React.FC<AttachmentDescriptionProps> = React.forwardRef<HTMLSpanElement, AttachmentDescriptionProps>(
(props, ref) => {
const { className, children, ...rest } = props;
const classes = useAttachmentDescriptionStyles();

return (
<span ref={ref} className={mergeClasses(attachmentDescriptionClassName, classes.root, className)} {...rest}>
{children}
</span>
);
},
);

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

export const useAttachmentHeaderStyles = makeStyles({
root: {
display: 'block',
fontSize: tokens.fontSizeBase300,
fontWeight: tokens.fontWeightSemibold,
lineHeight: 1.4286,
},
});
Loading
Loading