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: create MinimizableDialog component #2623

Merged
merged 3 commits into from
Aug 24, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions .changeset/nasty-berries-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@twilio-paste/minimizable-dialog': major
'@twilio-paste/core': minor
---

[Minimizable Dialog] Introducing the all-new Minimizable Dialog component! This component can be used for a chat widget so that users can keep the chat open while they interact with the rest of the page. 🎉
1 change: 1 addition & 0 deletions .codesandbox/ci.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"/packages/paste-core/layout/media-object",
"/packages/paste-core/components/menu",
"/packages/paste-core/primitives/menu",
"/packages/paste-core/components/minimizable-dialog",
"/packages/paste-core/components/modal",
"/packages/paste-core/primitives/modal-dialog",
"/packages/paste-core/primitives/non-modal-dialog",
Expand Down
1 change: 1 addition & 0 deletions cypress/integration/sitemap-vrt/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const SITEMAP = [
'/components/input/',
'/components/label/',
'/components/list/',
'/components/minimizable-dialog/',
'/components/media-object/',
'/components/pagination/',
'/components/modal/',
Expand Down
6 changes: 6 additions & 0 deletions packages/paste-codemods/tools/.cache/mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@
"StyledMenuItem": "@twilio-paste/core/menu",
"SubMenuButton": "@twilio-paste/core/menu",
"useMenuState": "@twilio-paste/core/menu",
"MinimizableDialog": "@twilio-paste/core/minimizable-dialog",
"MinimizableDialogButton": "@twilio-paste/core/minimizable-dialog",
"MinimizableDialogContainer": "@twilio-paste/core/minimizable-dialog",
"MinimizableDialogContent": "@twilio-paste/core/minimizable-dialog",
"MinimizableDialogHeader": "@twilio-paste/core/minimizable-dialog",
"useMinimizableDialogState": "@twilio-paste/core/minimizable-dialog",
"Modal": "@twilio-paste/core/modal",
"ModalBody": "@twilio-paste/core/modal",
"ModalContext": "@twilio-paste/core/modal",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import * as React from 'react';
import {act, fireEvent, render, screen, waitFor} from '@testing-library/react';
import {CustomizationProvider} from '@twilio-paste/customization';

import {
MinimizableDialog,
MinimizableDialogButton,
MinimizableDialogHeader,
MinimizableDialogContainer,
MinimizableDialogContent,
} from '../src';

const CustomizationWrapper: React.FC = ({children}) => (
<CustomizationProvider
baseTheme="default"
theme={TestTheme}
elements={{
MINIMIZABLE_DIALOG_BUTTON: {
backgroundColor: 'colorBackgroundDark',
},
MINIMIZABLE_DIALOG: {
backgroundColor: 'colorBackgroundPrimaryLighter',
},
MINIMIZABLE_DIALOG_CONTENT: {
margin: 'space0',
},
MINIMIZABLE_DIALOG_HEADER: {
padding: 'space80',
},
MINIMIZABLE_DIALOG_HEADER_HEADING: {
fontSize: 'fontSize50',
},
MINIMIZABLE_DIALOG_HEADER_CLOSE_BUTTON: {
backgroundColor: 'colorBackground',
},
MINIMIZABLE_DIALOG_HEADER_CLOSE_ICON: {
width: 'sizeIcon50',
},
MINIMIZABLE_DIALOG_HEADER_MINIMIZE_BUTTON: {
backgroundColor: 'colorBackground',
},
MINIMIZABLE_DIALOG_HEADER_MINIMIZE_ICON: {
width: 'sizeIcon50',
},
}}
>
{children}
</CustomizationProvider>
);

const MyCustomizationWrapper: React.FC = ({children}) => (
<CustomizationProvider
baseTheme="default"
theme={TestTheme}
elements={{
FOO_DIALOG_BUTTON: {
backgroundColor: 'colorBackgroundDark',
},
FOO_DIALOG: {
backgroundColor: 'colorBackgroundPrimaryLighter',
},
FOO_DIALOG_CONTENT: {
margin: 'space0',
},
FOO_DIALOG_HEADER: {
padding: 'space80',
},
FOO_DIALOG_HEADER_HEADING: {
fontSize: 'fontSize50',
},
FOO_DIALOG_HEADER_CLOSE_BUTTON: {
backgroundColor: 'colorBackground',
},
FOO_DIALOG_HEADER_CLOSE_ICON: {
width: 'sizeIcon50',
},
FOO_DIALOG_HEADER_MINIMIZE_BUTTON: {
backgroundColor: 'colorBackground',
},
FOO_DIALOG_HEADER_MINIMIZE_ICON: {
width: 'sizeIcon50',
},
}}
>
{children}
</CustomizationProvider>
);

describe('Customization', () => {
it('should have default element data attributes', () => {
render(
<MinimizableDialogContainer>
<MinimizableDialogButton variant="primary">Button</MinimizableDialogButton>
<MinimizableDialog aria-label="My custom dialog">
<MinimizableDialogHeader data-testid="dialog-header">My custom dialog</MinimizableDialogHeader>
<MinimizableDialogContent data-testid="dialog-contents">This is a dialog.</MinimizableDialogContent>
</MinimizableDialog>
</MinimizableDialogContainer>
);

const dialogButton = screen.getByRole('button', {name: 'Button'});
const minimizeButton = screen.getByRole('button', {name: 'minimize', hidden: true});
const closeButton = screen.getByRole('button', {name: 'close', hidden: true});
const dialog = screen.getByRole('dialog', {hidden: true});
const dialogContents = screen.getByTestId('dialog-contents');
const dialogHeader = screen.getByTestId('dialog-header');
const dialogHeading = screen.getByText('My custom dialog');

expect(dialogButton.getAttribute('data-paste-element')).toEqual('MINIMIZABLE_DIALOG_BUTTON');
expect(minimizeButton.getAttribute('data-paste-element')).toEqual('MINIMIZABLE_DIALOG_HEADER_MINIMIZE_BUTTON');
expect(closeButton.getAttribute('data-paste-element')).toEqual('MINIMIZABLE_DIALOG_HEADER_CLOSE_BUTTON');
expect(dialogContents.getAttribute('data-paste-element')).toEqual('MINIMIZABLE_DIALOG_CONTENT');
expect(dialogHeader.getAttribute('data-paste-element')).toEqual('MINIMIZABLE_DIALOG_HEADER');
expect(dialogHeading.getAttribute('data-paste-element')).toEqual('MINIMIZABLE_DIALOG_HEADER_HEADING');

expect(dialog.querySelector('[data-paste-element="MINIMIZABLE_DIALOG"]')).toBeInTheDocument();
expect(
minimizeButton.querySelector('[data-paste-element="MINIMIZABLE_DIALOG_HEADER_MINIMIZE_ICON"]')
).toBeInTheDocument();
expect(
closeButton.querySelector('[data-paste-element="MINIMIZABLE_DIALOG_HEADER_CLOSE_ICON"]')
).toBeInTheDocument();
});

it('should add custom styles to components', async () => {
render(
<MinimizableDialogContainer>
<MinimizableDialogButton variant="primary">Button</MinimizableDialogButton>
<MinimizableDialog aria-label="My custom dialog">
<MinimizableDialogHeader data-testid="dialog-header">My custom dialog</MinimizableDialogHeader>
<MinimizableDialogContent data-testid="dialog-contents">This is a dialog.</MinimizableDialogContent>
</MinimizableDialog>
</MinimizableDialogContainer>,
{wrapper: CustomizationWrapper}
);

const dialogButton = screen.getByRole('button', {name: 'Button'});

await waitFor(() => {
fireEvent.click(dialogButton);
});

const minimizeButton = screen.getByRole('button', {name: 'minimize'});
const closeButton = screen.getByRole('button', {name: 'close'});
const dialog = screen.getByRole('dialog').querySelector('[data-paste-element="MINIMIZABLE_DIALOG"]');
const dialogContents = screen.getByTestId('dialog-contents');
const dialogHeader = screen.getByTestId('dialog-header');
const dialogHeading = screen.getByText('My custom dialog');
const minimizeIcon = minimizeButton.querySelector('[data-paste-element="MINIMIZABLE_DIALOG_HEADER_MINIMIZE_ICON"]');
const closeIcon = closeButton.querySelector('[data-paste-element="MINIMIZABLE_DIALOG_HEADER_CLOSE_ICON"]');

expect(dialogButton).toHaveStyleRule('background-color', 'rgb(225, 227, 234)');
expect(dialogHeader).toHaveStyleRule('padding-top', '0.75rem');
expect(dialogHeading).toHaveStyleRule('font-size', '1.125rem');
expect(dialogContents).toHaveStyleRule('margin', '0');
expect(dialog).toHaveStyleRule('background-color', 'rgb(204, 228, 255)');
expect(minimizeButton).toHaveStyleRule('background-color', 'rgb(244, 244, 246)');
expect(minimizeIcon).toHaveStyleRule('width', '1.75rem');
expect(closeButton).toHaveStyleRule('background-color', 'rgb(244, 244, 246)');
expect(closeIcon).toHaveStyleRule('width', '1.75rem');
});

it('should set custom element data attributes', () => {
render(
<MinimizableDialogContainer>
<MinimizableDialogButton variant="primary" element="FOO_DIALOG_BUTTON">
Button
</MinimizableDialogButton>
<MinimizableDialog aria-label="My custom dialog" element="FOO_DIALOG">
<MinimizableDialogHeader data-testid="dialog-header" element="FOO_DIALOG_HEADER">
My custom dialog
</MinimizableDialogHeader>
<MinimizableDialogContent data-testid="dialog-contents" element="FOO_DIALOG_CONTENT">
This is a dialog.
</MinimizableDialogContent>
</MinimizableDialog>
</MinimizableDialogContainer>
);

const dialogButton = screen.getByRole('button', {name: 'Button'});
const minimizeButton = screen.getByRole('button', {name: 'minimize', hidden: true});
const closeButton = screen.getByRole('button', {name: 'close', hidden: true});
const dialog = screen.getByRole('dialog', {hidden: true});
const dialogContents = screen.getByTestId('dialog-contents');
const dialogHeader = screen.getByTestId('dialog-header');
const dialogHeading = screen.getByText('My custom dialog');

expect(dialogButton.getAttribute('data-paste-element')).toEqual('FOO_DIALOG_BUTTON');
expect(minimizeButton.getAttribute('data-paste-element')).toEqual('FOO_DIALOG_HEADER_MINIMIZE_BUTTON');
expect(closeButton.getAttribute('data-paste-element')).toEqual('FOO_DIALOG_HEADER_CLOSE_BUTTON');
expect(dialogContents.getAttribute('data-paste-element')).toEqual('FOO_DIALOG_CONTENT');
expect(dialogHeader.getAttribute('data-paste-element')).toEqual('FOO_DIALOG_HEADER');
expect(dialogHeading.getAttribute('data-paste-element')).toEqual('FOO_DIALOG_HEADER_HEADING');

expect(dialog.querySelector('[data-paste-element="FOO_DIALOG"]')).toBeInTheDocument();
expect(minimizeButton.querySelector('[data-paste-element="FOO_DIALOG_HEADER_MINIMIZE_ICON"]')).toBeInTheDocument();
expect(closeButton.querySelector('[data-paste-element="FOO_DIALOG_HEADER_CLOSE_ICON"]')).toBeInTheDocument();
});

it('should add custom styles to components with custom element data attributes', async () => {
render(
<MinimizableDialogContainer>
<MinimizableDialogButton variant="primary" element="FOO_DIALOG_BUTTON">
Button
</MinimizableDialogButton>
<MinimizableDialog aria-label="My custom dialog" element="FOO_DIALOG">
<MinimizableDialogHeader data-testid="dialog-header" element="FOO_DIALOG_HEADER">
My custom dialog
</MinimizableDialogHeader>
<MinimizableDialogContent data-testid="dialog-contents" element="FOO_DIALOG_CONTENT">
This is a dialog.
</MinimizableDialogContent>
</MinimizableDialog>
</MinimizableDialogContainer>,
{wrapper: MyCustomizationWrapper}
);

const dialogButton = screen.getByRole('button', {name: 'Button'});

await waitFor(() => {
fireEvent.click(dialogButton);
});

const minimizeButton = screen.getByRole('button', {name: 'minimize'});
const closeButton = screen.getByRole('button', {name: 'close'});
const dialog = screen.getByRole('dialog').querySelector('[data-paste-element="FOO_DIALOG"]');
const dialogContents = screen.getByTestId('dialog-contents');
const dialogHeader = screen.getByTestId('dialog-header');
const dialogHeading = screen.getByText('My custom dialog');
const minimizeIcon = minimizeButton.querySelector('[data-paste-element="FOO_DIALOG_HEADER_MINIMIZE_ICON"]');
const closeIcon = closeButton.querySelector('[data-paste-element="FOO_DIALOG_HEADER_CLOSE_ICON"]');

expect(dialogButton).toHaveStyleRule('background-color', 'rgb(225, 227, 234)');
expect(dialogHeader).toHaveStyleRule('padding-top', '0.75rem');
expect(dialogHeading).toHaveStyleRule('font-size', '1.125rem');
expect(dialogContents).toHaveStyleRule('margin', '0');
expect(dialog).toHaveStyleRule('background-color', 'rgb(204, 228, 255)');
expect(minimizeButton).toHaveStyleRule('background-color', 'rgb(244, 244, 246)');
expect(minimizeIcon).toHaveStyleRule('width', '1.75rem');
expect(closeButton).toHaveStyleRule('background-color', 'rgb(244, 244, 246)');
expect(closeIcon).toHaveStyleRule('width', '1.75rem');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import * as React from 'react';
import {render, screen, fireEvent, waitFor} from '@testing-library/react';

import {
MinimizableDialog,
MinimizableDialogButton,
MinimizableDialogHeader,
MinimizableDialogContainer,
MinimizableDialogContent,
} from '../src';

import {StateHookExample} from '../stories/index.stories';

describe('MinimizableDialog', () => {
describe('Render', () => {
it('should render a dialog button and dialog with aria attributes', async () => {
render(
<MinimizableDialogContainer>
<MinimizableDialogButton variant="primary">Button</MinimizableDialogButton>
<MinimizableDialog aria-label="My custom dialog">
<MinimizableDialogHeader>My custom dialog</MinimizableDialogHeader>
<MinimizableDialogContent>This is a dialog.</MinimizableDialogContent>
</MinimizableDialog>
</MinimizableDialogContainer>
);

const dialogButton = screen.getByRole('button', {name: 'Button'});
const dialog = screen.getByRole('dialog', {hidden: true});

expect(dialogButton.getAttribute('aria-haspopup')).toEqual('dialog');
expect(dialogButton.getAttribute('aria-controls')).toEqual(dialog.id);
expect(dialogButton.getAttribute('aria-expanded')).toEqual('false');

expect(dialog).not.toBeVisible();
await waitFor(() => {
fireEvent.click(dialogButton);
});
expect(dialog).toBeVisible();
});

it('should render a dialog and toggle visible and minimized with external buttons', async () => {
render(<StateHookExample />);

const showButton = screen.getByRole('button', {name: 'Open dialog'});
const closeButton = screen.getByRole('button', {name: 'Close dialog'});
const minimizeButton = screen.getByRole('button', {name: 'Minimize dialog'});
const expandButton = screen.getByRole('button', {name: 'Expand dialog'});
const dialog = screen.getByRole('dialog', {hidden: true});
const dialogContents = screen.getByTestId('dialog-contents');
const dialogHeader = screen.getByTestId('dialog-header');

expect(dialog).not.toBeVisible();
await waitFor(() => {
fireEvent.click(showButton);
});
expect(dialog).toBeVisible();

await waitFor(() => {
fireEvent.click(minimizeButton);
});
expect(dialogContents).not.toBeVisible();
expect(dialogHeader).toBeVisible();

await waitFor(() => {
fireEvent.click(expandButton);
});
expect(dialogContents).toBeVisible();
expect(dialogHeader).toBeVisible();

await waitFor(() => {
fireEvent.click(closeButton);
});
expect(dialog).not.toBeVisible();
});
});

describe('i18n', () => {
it('should have default dismiss and minimize button text', async () => {
render(
<MinimizableDialogContainer>
<MinimizableDialogButton variant="primary">Button</MinimizableDialogButton>
<MinimizableDialog aria-label="My custom dialog">
<MinimizableDialogHeader>My custom dialog</MinimizableDialogHeader>
<MinimizableDialogContent>This is a dialog.</MinimizableDialogContent>
</MinimizableDialog>
</MinimizableDialogContainer>
);

const dismissButton = screen.getByRole('button', {name: 'close', hidden: true});
expect(dismissButton).toBeDefined();

const minimizeButton = screen.getByRole('button', {name: 'minimize', hidden: true});
expect(minimizeButton).toBeDefined();
});
});
});
3 changes: 3 additions & 0 deletions packages/paste-core/components/minimizable-dialog/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const {build} = require('../../../../tools/build/esbuild');

build(require('./package.json'));
Loading