-
Notifications
You must be signed in to change notification settings - Fork 114
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(modal): customization #1903
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
36ff66b
feat(modal): customizaiton
andioneto 88542da
feat(modal): iterate on element names
andioneto b233d62
feat(modal): add base story
andioneto 4d10503
feat(modal): finish stories
andioneto 1d6bd4c
feat(modal): small cleanup
andioneto b9d7d46
feat(modal): remove conditional for prop types
andioneto b661cd2
feat(modal): add changeset
andioneto b92397d
feat(modal): add unit tests for customization
andioneto 704aa55
feat(modal): add missing proptype for element
andioneto db70ace
feat(modal): customization story cleanup
andioneto f6e1281
feat(modal): oops forgot matchers
andioneto 7b5dc97
feat(modal): add comment about jest mock
andioneto File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
'@twilio-paste/modal': patch | ||
'@twilio-paste/core': patch | ||
--- | ||
|
||
[Modal] Enable Component to respect element customizations set on the customization provider. Component now enables setting an element name on the underlying HTML element and checks the emotion theme object to determine whether it should merge in custom styles to the ones set by the component author. |
166 changes: 166 additions & 0 deletions
166
packages/paste-core/components/modal/__tests__/customization.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
import * as React from 'react'; | ||
|
||
import {render, screen} from '@testing-library/react'; | ||
import {CustomizationProvider} from '@twilio-paste/customization'; | ||
import {matchers} from 'jest-emotion'; | ||
|
||
import {BaseModal, initStyles} from '../stories/customization.stories'; | ||
|
||
expect.extend(matchers); | ||
|
||
jest.mock('@twilio-paste/modal-dialog-primitive', () => { | ||
// Mocking the portal as a div so it renders within the body of the rendered test fragment, rather than using Portal behavior. | ||
const actual = jest.requireActual('@twilio-paste/modal-dialog-primitive'); | ||
const {forwardRef: mockForwardRef} = jest.requireActual('react'); | ||
const MockModalDialogPrimitiveOverlay = mockForwardRef( | ||
( | ||
{ | ||
children, | ||
'data-paste-element': dataPasteElement, | ||
style, | ||
className, | ||
}: {children: any; 'data-paste-element': string; style: any; className: string}, | ||
ref: any | ||
) => ( | ||
<div | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mocking the react portal for this test. |
||
data-testid="mock-reach-dialog-overlay" | ||
data-paste-element={dataPasteElement} | ||
style={style} | ||
ref={ref} | ||
className={className} | ||
> | ||
{children} | ||
</div> | ||
) | ||
); | ||
return { | ||
...actual, | ||
ModalDialogPrimitiveOverlay: MockModalDialogPrimitiveOverlay, | ||
}; | ||
}); | ||
|
||
describe('Modal Customization', () => { | ||
describe('"data-paste-element" HTML attributes', () => { | ||
it('Should add the correct "data-paste-element" attribute when element prop is undefined', () => { | ||
render(<BaseModal size="default" />); | ||
|
||
expect(screen.getByTestId('mock-reach-dialog-overlay').getAttribute('data-paste-element')).toEqual( | ||
'MODAL_OVERLAY' | ||
); | ||
expect(screen.getByTestId('modal-test-id').getAttribute('data-paste-element')).toEqual('MODAL'); | ||
expect(screen.getByTestId('modal-header-test-id').getAttribute('data-paste-element')).toEqual('MODAL_HEADER'); | ||
expect(screen.getByTestId('modal-heading-test-id').getAttribute('data-paste-element')).toEqual('MODAL_HEADING'); | ||
expect(screen.getByTestId('modal-body-test-id').getAttribute('data-paste-element')).toEqual('MODAL_BODY'); | ||
|
||
const modalFooter = screen.getByTestId('modal-footer-test-id'); | ||
expect(modalFooter.getAttribute('data-paste-element')).toEqual('MODAL_FOOTER'); | ||
|
||
const modalFooterActions = modalFooter.firstChild as HTMLElement; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this different from using |
||
expect(modalFooterActions.getAttribute('data-paste-element')).toEqual('MODAL_FOOTER_ACTIONS'); | ||
|
||
const modalFooterActionItemOne = modalFooterActions.firstChild as HTMLElement; | ||
const modalFooterActionItemTwo = modalFooterActions.lastChild as HTMLElement; | ||
expect(modalFooterActionItemOne.getAttribute('data-paste-element')).toEqual('MODAL_FOOTER_ACTIONS_ITEM'); | ||
expect(modalFooterActionItemTwo.getAttribute('data-paste-element')).toEqual('MODAL_FOOTER_ACTIONS_ITEM'); | ||
}); | ||
|
||
it('Should add the correct "data-paste-element" attribute when element prop is defined', () => { | ||
render(<BaseModal size="default" element="CUSTOM_TEST_MODAL" />); | ||
|
||
expect(screen.getByTestId('mock-reach-dialog-overlay').getAttribute('data-paste-element')).toEqual( | ||
'CUSTOM_TEST_MODAL_OVERLAY' | ||
); | ||
expect(screen.getByTestId('modal-test-id').getAttribute('data-paste-element')).toEqual('CUSTOM_TEST_MODAL'); | ||
expect(screen.getByTestId('modal-header-test-id').getAttribute('data-paste-element')).toEqual( | ||
'CUSTOM_TEST_MODAL_HEADER' | ||
); | ||
expect(screen.getByTestId('modal-heading-test-id').getAttribute('data-paste-element')).toEqual( | ||
'CUSTOM_TEST_MODAL_HEADING' | ||
); | ||
expect(screen.getByTestId('modal-body-test-id').getAttribute('data-paste-element')).toEqual( | ||
'CUSTOM_TEST_MODAL_BODY' | ||
); | ||
|
||
const modalFooter = screen.getByTestId('modal-footer-test-id'); | ||
const modalFooterActions = modalFooter.firstChild as HTMLElement; | ||
const modalFooterActionItemOne = modalFooterActions.firstChild as HTMLElement; | ||
const modalFooterActionItemTwo = modalFooterActions.lastChild as HTMLElement; | ||
|
||
expect(modalFooter.getAttribute('data-paste-element')).toEqual('CUSTOM_TEST_MODAL_FOOTER'); | ||
expect(modalFooterActions.getAttribute('data-paste-element')).toEqual('CUSTOM_TEST_MODAL_FOOTER_ACTIONS'); | ||
expect(modalFooterActionItemOne.getAttribute('data-paste-element')).toEqual( | ||
'CUSTOM_TEST_MODAL_FOOTER_ACTIONS_ITEM' | ||
); | ||
expect(modalFooterActionItemTwo.getAttribute('data-paste-element')).toEqual( | ||
'CUSTOM_TEST_MODAL_FOOTER_ACTIONS_ITEM' | ||
); | ||
}); | ||
}); | ||
|
||
describe('Custom styles', () => { | ||
it('Should apply correct style rules to normal size variant', () => { | ||
render(<BaseModal size="default" />, { | ||
wrapper: ({children}) => ( | ||
<CustomizationProvider | ||
// @ts-expect-error global test variable | ||
theme={TestTheme} | ||
elements={initStyles('MODAL')} | ||
andioneto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
> | ||
{children} | ||
</CustomizationProvider> | ||
), | ||
}); | ||
|
||
expect(screen.getByTestId('mock-reach-dialog-overlay')).toHaveStyleRule('background-color', 'rgb(6,3,58)'); | ||
|
||
expect(screen.getByTestId('modal-test-id')).toHaveStyleRule('border-radius', '8px'); | ||
expect(screen.getByTestId('modal-test-id')).toHaveStyleRule('box-shadow', '0 16px 24px 0 rgba(18,28,45,0.2)'); | ||
expect(screen.getByTestId('modal-test-id')).toHaveStyleRule('border-color', 'rgb(96,107,133)'); | ||
|
||
expect(screen.getByTestId('modal-header-test-id')).toHaveStyleRule('border-width', '0'); | ||
expect(screen.getByTestId('modal-header-test-id')).toHaveStyleRule('border-style', 'none'); | ||
expect(screen.getByTestId('modal-header-test-id')).toHaveStyleRule('border-color', 'transparent'); | ||
|
||
expect(screen.getByTestId('modal-heading-test-id')).toHaveStyleRule('font-size', '3rem'); | ||
|
||
expect(screen.getByTestId('modal-body-test-id')).toHaveStyleRule('padding-right', '1.25rem'); | ||
expect(screen.getByTestId('modal-body-test-id')).toHaveStyleRule('padding-left', '1.25rem'); | ||
|
||
const modalFooter = screen.getByTestId('modal-footer-test-id'); | ||
const modalFooterActions = modalFooter.firstChild as HTMLElement; | ||
const modalFooterActionItemOne = modalFooterActions.firstChild as HTMLElement; | ||
const modalFooterActionItemTwo = modalFooterActions.lastChild as HTMLElement; | ||
|
||
expect(modalFooter).toHaveStyleRule('border-width', '0'); | ||
expect(modalFooter).toHaveStyleRule('border-style', 'none'); | ||
expect(modalFooter).toHaveStyleRule('border-color', 'transparent'); | ||
|
||
expect(modalFooterActions).toHaveStyleRule('justify-content', 'flex-start'); | ||
|
||
expect(modalFooterActionItemOne).toHaveStyleRule('padding-left', '0', {target: ':first-of-type'}); | ||
expect(modalFooterActionItemOne).toHaveStyleRule('padding-right', '0.75rem'); | ||
|
||
expect(modalFooterActionItemTwo).toHaveStyleRule('padding-left', '0.75rem'); | ||
expect(modalFooterActionItemTwo).toHaveStyleRule('padding-right', '0.75rem'); | ||
}); | ||
|
||
it('Should apply correct style rules to wide size variant', () => { | ||
render(<BaseModal size="wide" />, { | ||
wrapper: ({children}) => ( | ||
<CustomizationProvider | ||
// @ts-expect-error global test variable | ||
theme={TestTheme} | ||
elements={initStyles('MODAL')} | ||
> | ||
{children} | ||
</CustomizationProvider> | ||
), | ||
}); | ||
|
||
expect(screen.getByTestId('mock-reach-dialog-overlay')).toHaveStyleRule('background-color', 'rgb(244,244,246)'); | ||
|
||
expect(screen.getByTestId('modal-test-id')).toHaveStyleRule('max-width', 'unset'); | ||
expect(screen.getByTestId('modal-test-id')).toHaveStyleRule('width', '70%'); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,27 @@ | ||
import * as React from 'react'; | ||
import * as PropTypes from 'prop-types'; | ||
import {Box, safelySpreadBoxProps} from '@twilio-paste/box'; | ||
import type {BoxElementProps} from '@twilio-paste/box'; | ||
import {modalBodyStyles} from './styles'; | ||
|
||
export interface ModalBodyProps extends React.HTMLAttributes<HTMLDivElement> { | ||
children: NonNullable<React.ReactNode>; | ||
element?: BoxElementProps['element']; | ||
} | ||
const ModalBody = React.forwardRef<HTMLDivElement, ModalBodyProps>(({children, ...props}, ref) => { | ||
return ( | ||
<Box {...safelySpreadBoxProps(props)} {...modalBodyStyles} as="div" ref={ref}> | ||
{children} | ||
</Box> | ||
); | ||
}); | ||
const ModalBody = React.forwardRef<HTMLDivElement, ModalBodyProps>( | ||
({children, element = 'MODAL_BODY', ...props}, ref) => { | ||
return ( | ||
<Box {...safelySpreadBoxProps(props)} {...modalBodyStyles} as="div" element={element} ref={ref}> | ||
{children} | ||
</Box> | ||
); | ||
} | ||
); | ||
ModalBody.displayName = 'ModalBody'; | ||
|
||
if (process.env.NODE_ENV === 'development') { | ||
ModalBody.propTypes = { | ||
children: PropTypes.node.isRequired, | ||
}; | ||
} | ||
ModalBody.propTypes = { | ||
children: PropTypes.node.isRequired, | ||
element: PropTypes.string, | ||
}; | ||
|
||
export {ModalBody}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,27 @@ | ||
import * as React from 'react'; | ||
import * as PropTypes from 'prop-types'; | ||
import {Box, safelySpreadBoxProps} from '@twilio-paste/box'; | ||
import type {BoxElementProps} from '@twilio-paste/box'; | ||
import {modalFooterStyles} from './styles'; | ||
|
||
export interface ModalFooterProps extends React.HTMLAttributes<HTMLDivElement> { | ||
children: NonNullable<React.ReactNode>; | ||
element?: BoxElementProps['element']; | ||
} | ||
const ModalFooter = React.forwardRef<HTMLDivElement, ModalFooterProps>(({children, ...props}, ref) => { | ||
return ( | ||
<Box {...safelySpreadBoxProps(props)} {...modalFooterStyles} as="footer" ref={ref}> | ||
{children} | ||
</Box> | ||
); | ||
}); | ||
const ModalFooter = React.forwardRef<HTMLDivElement, ModalFooterProps>( | ||
({children, element = 'MODAL_FOOTER', ...props}, ref) => { | ||
return ( | ||
<Box {...safelySpreadBoxProps(props)} {...modalFooterStyles} as="footer" element={element} ref={ref}> | ||
{children} | ||
</Box> | ||
); | ||
} | ||
); | ||
ModalFooter.displayName = 'ModalFooter'; | ||
|
||
if (process.env.NODE_ENV === 'development') { | ||
ModalFooter.propTypes = { | ||
children: PropTypes.node.isRequired, | ||
}; | ||
} | ||
ModalFooter.propTypes = { | ||
children: PropTypes.node.isRequired, | ||
element: PropTypes.string, | ||
}; | ||
|
||
export {ModalFooter}; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I'm a little unsure on why this needs to be mocked, so providing some sort of commentary on the why might be helpful in a code comment.