-
Notifications
You must be signed in to change notification settings - Fork 10
Add Dialog component #343
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
Merged
Merged
Add Dialog component #343
Changes from all commits
Commits
Show all changes
45 commits
Select commit
Hold shift + click to select a range
bebd54f
feat(dialog): initial implementation
igdmdimitrov da283ba
Merge branch 'master' into dmdimitrov/dialog-component
igdmdimitrov fdd723e
Merge branch 'master' into dmdimitrov/dialog-component
simeonoff 0b1e4c7
refactor(dialog): fix markup and add light themes
simeonoff 536fce9
refactor(dialog): add dark themes
simeonoff ce50362
fix(dialog): bootstrap shows when closed
simeonoff 365f6da
Merge branch 'master' into dmdimitrov/dialog-component
simeonoff c19fb82
Merge branch 'master' into dmdimitrov/dialog-component
rkaraivanov 18ed1b9
Merge branch 'master' into dmdimitrov/dialog-component
igdmdimitrov 8e5011c
feat(dialog): component implementation
onlyexeption 3f25164
feat(dialog): handle open state
onlyexeption a9f8f23
feat(dialog): add role and aria props
onlyexeption 7ce0bae
chore: add default value to aria-labelledby
onlyexeption 66c2b32
Merge branch 'master' into dmdimitrov/dialog-component
igdmdimitrov 7bf8922
feat(dialog): added tests
igdmdimitrov eb0071f
push latest changes
onlyexeption 85463df
Merge branch 'master' into dmdimitrov/dialog-component
onlyexeption 6ff0ca9
push latest changes
onlyexeption 1746411
fix role property
onlyexeption 76a3ce5
Merge branch 'master' into dmdimitrov/dialog-component
simeonoff b039383
Merge branch 'master' into dmdimitrov/dialog-component
rkaraivanov 81a6d54
Merge branch 'master' into dmdimitrov/dialog-component
igdmdimitrov 7c8d141
feat(dialog): update dialog themes
didimmova b55494c
feat(dialog): remove dark themes
didimmova d2f49c2
Merge branch 'master' into dmdimitrov/dialog-component
simeonoff 8b2b4ed
feat(dialog): fix backdrop
didimmova e96cbb1
test(dialog): fix failing test
didimmova c004a24
Merge branch 'master' into dmdimitrov/dialog-component
simeonoff d3954c7
feat(dialog): use hidden attribute
didimmova 6cd62c5
Merge branch 'dmdimitrov/dialog-component' of https://github.com/Igni…
didimmova cea8aa2
Merge branch 'master' into dmdimitrov/dialog-component
simeonoff a48673f
test(dialog): fix test
didimmova 5e5db61
Merge branch 'master' into dmdimitrov/dialog-component
5ffc60c
Merge branch 'master' into dmdimitrov/dialog-component
rkaraivanov a9a4b53
chore(*): refactor and address PR comments
igdmdimitrov b28027c
chore(*): address PR comments
igdmdimitrov cf41764
Merge branch 'master' into dmdimitrov/dialog-component
igdmdimitrov 2242282
feat(dialog): render ok button if no content
igdmdimitrov 567f9ce
refactor: Dialog features
rkaraivanov f03a8ee
refactor: Default action button location
rkaraivanov 3997cfd
Merge branch 'master' into dmdimitrov/dialog-component
igdmdimitrov f4d0d7d
feat(dialog): add more tests
igdmdimitrov 2e01fd7
Merge branch 'master' into dmdimitrov/dialog-component
igdmdimitrov 9b0bff1
refactor: Default action button and events
rkaraivanov 344ea88
Merge branch 'master' into dmdimitrov/dialog-component
ChronosSF 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 hidden or 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,346 @@ | ||
| import { | ||
| elementUpdated, | ||
| expect, | ||
| fixture, | ||
| html, | ||
| unsafeStatic, | ||
| } from '@open-wc/testing'; | ||
| import sinon from 'sinon'; | ||
| import { defineComponents, IgcDialogComponent } from '../../index.js'; | ||
|
|
||
| describe('Dialog component', () => { | ||
| const fireMouseEvent = (type: string, opts: MouseEventInit) => | ||
| new MouseEvent(type, opts); | ||
| const getBoundingRect = (el: Element) => el.getBoundingClientRect(); | ||
|
|
||
| before(() => { | ||
| defineComponents(IgcDialogComponent); | ||
| }); | ||
|
|
||
| let dialog: IgcDialogComponent; | ||
| let dialogEl: HTMLDialogElement; | ||
|
|
||
| describe('', () => { | ||
| beforeEach(async () => { | ||
| dialog = await createDialogComponent(); | ||
| dialogEl = dialog.shadowRoot!.querySelector( | ||
| 'dialog' | ||
| ) as HTMLDialogElement; | ||
| }); | ||
|
|
||
| it('passes the a11y audit', async () => { | ||
| const dialog = await fixture<IgcDialogComponent>( | ||
| html`<igc-dialog></igc-dialog>` | ||
| ); | ||
|
|
||
| await expect(dialog).shadowDom.to.be.accessible(); | ||
| }); | ||
|
|
||
| it('should render content inside the dialog', async () => { | ||
| const content = 'Dialog content'; | ||
| dialog = await createDialogComponent( | ||
| `<igc-dialog><span>${content}</span></igc-dialog>` | ||
| ); | ||
|
|
||
| dialog.show(); | ||
| await elementUpdated(dialog); | ||
|
|
||
| expect(dialog).dom.to.have.text(content); | ||
| expect(dialog).dom.to.equal( | ||
| ` | ||
| <igc-dialog> | ||
| <span> | ||
| Dialog content | ||
| </span> | ||
| </igc-dialog>`, | ||
| { | ||
| ignoreAttributes: [ | ||
| 'variant', | ||
| 'aria-label', | ||
| 'aria-disabled', | ||
| 'aria-hidden', | ||
| 'aria-labelledby', | ||
| 'part', | ||
| 'role', | ||
| 'size', | ||
| 'id', | ||
| 'hidden', | ||
| 'open', | ||
| ], | ||
| } | ||
| ); | ||
| }); | ||
|
|
||
| it('`hide-default-action` correctly toggles DOM structure', async () => { | ||
| dialog = await fixture<IgcDialogComponent>( | ||
| html`<igc-dialog>Message</igc-dialog>` | ||
| ); | ||
|
|
||
| const footer = dialog.shadowRoot!.querySelector('footer'); | ||
|
|
||
| expect(footer).dom.to.equal( | ||
| `<footer> | ||
| <slot name="footer"> | ||
| <igc-button>OK</igc-button> | ||
| </slot> | ||
| </footer>`, | ||
| { | ||
| ignoreAttributes: ['part', 'variant', 'size'], | ||
| } | ||
| ); | ||
|
|
||
| dialog.hideDefaultAction = true; | ||
| await elementUpdated(dialog); | ||
|
|
||
| expect(footer).dom.to.equal( | ||
| `<footer> | ||
| <slot name="footer"> | ||
| </slot> | ||
| </footer>`, | ||
| { | ||
| ignoreAttributes: ['part'], | ||
| } | ||
| ); | ||
| }); | ||
|
|
||
| it('renders a dialog element internally with default button if no content is provided', async () => { | ||
| await expect(dialog).shadowDom.to.be.accessible(); | ||
| expect(dialog).shadowDom.to.equal( | ||
| ` | ||
| <div></div> | ||
| <dialog> | ||
| <header> | ||
| <slot name="title"><span></span></slot> | ||
| </header> | ||
| <section> | ||
| <slot></slot> | ||
| </section> | ||
| <footer> | ||
| <slot name="footer"> | ||
| <igc-button> | ||
| OK | ||
| </igc-button> | ||
| </slot> | ||
| </footer> | ||
| </dialog>`, | ||
| { | ||
| ignoreAttributes: [ | ||
| 'variant', | ||
| 'aria-label', | ||
| 'aria-disabled', | ||
| 'aria-hidden', | ||
| 'aria-labelledby', | ||
| 'part', | ||
| 'role', | ||
| 'size', | ||
| 'id', | ||
| 'hidden', | ||
| ], | ||
| } | ||
| ); | ||
| }); | ||
|
|
||
| it('show method should open the dialog', async () => { | ||
| dialog.open = false; | ||
| await elementUpdated(dialog); | ||
|
|
||
| dialog.show(); | ||
| await elementUpdated(dialog); | ||
| expect(dialog.open).to.eq(true); | ||
| }); | ||
|
|
||
| it('hide method should close the dialog', async () => { | ||
| dialog.open = true; | ||
| await elementUpdated(dialog); | ||
|
|
||
| dialog.hide(); | ||
| await elementUpdated(dialog); | ||
| expect(dialog.open).to.eq(false); | ||
| }); | ||
|
|
||
| it('toggle method should toggle the dialog', async () => { | ||
| dialog.open = true; | ||
| await elementUpdated(dialog); | ||
|
|
||
| dialog.toggle(); | ||
| await elementUpdated(dialog); | ||
| expect(dialog.open).to.eq(false); | ||
|
|
||
| dialog.open = false; | ||
| await elementUpdated(dialog); | ||
|
|
||
| dialog.toggle(); | ||
| await elementUpdated(dialog); | ||
| expect(dialog.open).to.eq(true); | ||
| }); | ||
|
|
||
| it('is created with the proper default values', async () => { | ||
| expect(dialog.closeOnEscape).to.equal(true); | ||
| expect(dialog.closeOnOutsideClick).to.equal(false); | ||
| expect(dialog.title).to.be.undefined; | ||
| expect(dialog.open).to.equal(false); | ||
| expect(dialog.returnValue).to.be.undefined; | ||
|
|
||
| const header = dialog.shadowRoot?.querySelector('header') as HTMLElement; | ||
| expect(dialogEl.getAttribute('aria-labelledby')).to.equal( | ||
| header.getAttribute('id') | ||
| ); | ||
| }); | ||
|
|
||
| it('has correct aria label and role', async () => { | ||
| dialog.ariaLabel = 'ariaLabel'; | ||
| dialog.open = true; | ||
| await elementUpdated(dialog); | ||
|
|
||
| expect(dialogEl.getAttribute('aria-label')).to.equal('ariaLabel'); | ||
| expect(dialogEl.getAttribute('role')).to.equal('dialog'); | ||
| }); | ||
|
|
||
| it('does not emit events through API calls', async () => { | ||
| const spy = sinon.spy(dialog, 'emitEvent'); | ||
| dialog.show(); | ||
| await elementUpdated(dialog); | ||
|
|
||
| expect(dialog.open).to.be.true; | ||
| expect(spy.callCount).to.equal(0); | ||
|
|
||
| dialog.hide(); | ||
| await elementUpdated(dialog); | ||
|
|
||
| expect(dialog.open).to.be.false; | ||
| expect(spy.callCount).to.equal(0); | ||
|
|
||
| dialog.open = true; | ||
| await elementUpdated(dialog); | ||
|
|
||
| expect(dialog.open).to.be.true; | ||
| expect(spy.callCount).to.equal(0); | ||
|
|
||
| dialog.open = false; | ||
| await elementUpdated(dialog); | ||
|
|
||
| expect(dialog.open).to.be.false; | ||
| expect(spy.callCount).to.equal(0); | ||
| }); | ||
|
|
||
| it('default action button emits closing events', async () => { | ||
| const spy = sinon.spy(dialog, 'emitEvent'); | ||
| dialog.show(); | ||
| await elementUpdated(dialog); | ||
|
|
||
| dialog.shadowRoot!.querySelector('igc-button')!.click(); | ||
| await elementUpdated(dialog); | ||
|
|
||
| expect(spy.callCount).to.equal(2); | ||
| expect(spy.firstCall).calledWith('igcClosing'); | ||
| expect(spy.secondCall).calledWith('igcClosed'); | ||
| }); | ||
|
|
||
| it('cancels closing event correctly', async () => { | ||
| dialog.toggle(); | ||
| await elementUpdated(dialog); | ||
| expect(dialog.open).to.be.true; | ||
|
|
||
| const eventSpy = sinon.spy(dialog, 'emitEvent'); | ||
|
|
||
| dialog.addEventListener('igcClosing', (ev) => { | ||
| ev.preventDefault(); | ||
| }); | ||
|
|
||
| dialog.shadowRoot!.querySelector('igc-button')!.click(); | ||
| await elementUpdated(dialog); | ||
| expect(eventSpy).calledOnceWith('igcClosing'); | ||
| }); | ||
|
|
||
| it('can cancel `igcClosing` event when clicking outside', async () => { | ||
| dialog.show(); | ||
| await elementUpdated(dialog); | ||
|
|
||
| dialog.closeOnOutsideClick = true; | ||
| await elementUpdated(dialog); | ||
|
|
||
| const eventSpy = sinon.spy(dialog, 'emitEvent'); | ||
| dialog.addEventListener('igcClosing', (e) => e.preventDefault()); | ||
|
|
||
| const { x, y } = getBoundingRect(dialog); | ||
| dialogEl.dispatchEvent( | ||
| fireMouseEvent('click', { | ||
| bubbles: true, | ||
| composed: true, | ||
| clientX: x - 1, | ||
| clientY: y - 1, | ||
| }) | ||
| ); | ||
| await elementUpdated(dialog); | ||
|
|
||
| expect(eventSpy).calledWith('igcClosing'); | ||
| expect(eventSpy).not.calledWith('igcClosed'); | ||
| }); | ||
|
|
||
| it('does not close the dialog on clicking outside when `closeOnOutsideClick` is false.', async () => { | ||
| dialog.show(); | ||
| await elementUpdated(dialog); | ||
|
|
||
| dialog.closeOnOutsideClick = false; | ||
| await elementUpdated(dialog); | ||
|
|
||
| const { x, y } = getBoundingRect(dialog); | ||
| dialogEl.dispatchEvent( | ||
| fireMouseEvent('click', { | ||
| bubbles: true, | ||
| composed: true, | ||
| clientX: x - 1, | ||
| clientY: y - 1, | ||
| }) | ||
| ); | ||
| await elementUpdated(dialog); | ||
|
|
||
| expect(dialog.open).to.be.true; | ||
| }); | ||
|
|
||
| it('closes the dialog on clicking outside when `closeOnOutsideClick` is true.', async () => { | ||
| dialog.show(); | ||
| await elementUpdated(dialog); | ||
|
|
||
| dialog.closeOnOutsideClick = true; | ||
| await elementUpdated(dialog); | ||
|
|
||
| const { x, y } = getBoundingRect(dialog); | ||
| dialogEl.dispatchEvent( | ||
| fireMouseEvent('click', { | ||
| bubbles: true, | ||
| composed: true, | ||
| clientX: x - 1, | ||
| clientY: y - 1, | ||
| }) | ||
| ); | ||
| await elementUpdated(dialog); | ||
|
|
||
| expect(dialog.open).to.be.false; | ||
| }); | ||
|
|
||
| it('closes the dialog when form with method=dialog is submitted', async () => { | ||
| dialog = await createDialogComponent(` | ||
| <igc-dialog> | ||
| <igc-form id="form" method="dialog"> | ||
| <igc-button type="submit">Confirm</igc-button> | ||
| </igc-form> | ||
| </igc-dialog> | ||
| `); | ||
| await elementUpdated(dialog); | ||
|
|
||
| dialog.show(); | ||
| await elementUpdated(dialog); | ||
|
|
||
| const form = document.getElementById('form'); | ||
| form?.dispatchEvent(new Event('igcSubmit')); | ||
| await elementUpdated(dialog); | ||
|
|
||
| expect(dialog.open).to.eq(false); | ||
| }); | ||
|
|
||
| const createDialogComponent = (template = `<igc-dialog></igc-dialog>`) => { | ||
| return fixture<IgcDialogComponent>(html`${unsafeStatic(template)}`); | ||
| }; | ||
| }); | ||
| }); | ||
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.
Confused. No way this is actually testing if the dialog has the content since it's not looking in the shadow DOM - so not really checking if the content is inside the dialog for real.
Same line of thinking for the second check - which again tests the light DOM for the very same nodes that were used in
createDialogComponent, thus testing if the fixture and native DOM node creation works, kinda pointless.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.
PS: Tested by deleting the default slot in the dialog, this test still passed :)