diff --git a/cypress/drafts/Menu/position.feature b/cypress/drafts/Menu/position.feature deleted file mode 100644 index d8318f564f..0000000000 --- a/cypress/drafts/Menu/position.feature +++ /dev/null @@ -1,51 +0,0 @@ -Feature: Position of a menu component - - # default max width: 380px; default max height: 380px - Background: - Given the menu component has a height and width of its default maximum - - Scenario: Default rendering - Given there is enough space between the anchor's bottom and the body's bottom to fit the default maximum - When the menu is opened - Then the menu is below the anchor - And the left of the menu is aligned with the left of the anchor - - Scenario: Flipped vertically - Given there is not enough space between the anchor's bottom and the body's bottom to fit the default maximum - And there is not enough space between the anchor's top and the body's top to fit the default maximum - When the menu is opened - Then the menu is above the anchor - And the left of the menu is aligned with the left of the anchor - - Scenario: Less than 368px and more than 50px available space below and above anchor - Given there is not enough space between the anchor's bottom and the body's bottom to fit the default maximum - And there is not enough space between the anchor's top and the body's top to fit the default maximum - But there is more than 50px of available space between the anchor's bottom and the body's bottom - When the menu is opened - Then the menu is below the anchor - And the height of the menu is reduced to fit - - # ¯\_(ツ)_/¯ - # This will cause the menu always to be off screen, but that's the apps fault - Scenario: Less than 50px available space below and above anchor - Given there is not enough space between the anchor's top and the body's top to fit the default maximum - And there is not enough space between the anchor's bottom and the body's bottom to fit the default maximum - When the menu is opened - Then the menu is below the anchor - And the heght of the menu is not reduced below 50px - - Scenario: Flipped horizontally - Given the space between the anchor's right and the body's right is less than the horizontal minimum space - And the space between the anchor's left and the body's left is at least the horizontal minimum space - When the menu is opened - Then the right of the menu is aligned with the right of the anchor - And the menu is below the anchor - - # ¯\_(ツ)_/¯ - # This will cause the menu always to be off screen, but that's the apps fault - Scenario: Forced body overflow - Given the space between the anchor's right and the body's right is less than the horizontal minimum space - And the space between the anchor's left and the body's left is less than the horizontal minimum space - When the menu is opened - Then the left of the menu is aligned with the left of the anchor - And the menu is rendered below the anchor diff --git a/cypress/drafts/Popover/position.feature b/cypress/drafts/Popover/position.feature deleted file mode 100644 index 70b8df5165..0000000000 --- a/cypress/drafts/Popover/position.feature +++ /dev/null @@ -1,31 +0,0 @@ -Feature: Popover positioning - - Background: - Given the popover has a width of 360px and height of 200px - - Scenario: Spacing between anchor and popover - When the anchor is clicked - Then there is some space between the anchor and the popover - - Scenario: Default positioning - Given there is enough space between the anchor's top and the body's top to fit the Popover - When the anchor is clicked - Then the popover is rendered above the the anchor - And the horizontal center of the popover is aligned with the horizontal center of the anchor - - Scenario: Flipped vertical - Given there is not enough space between the anchor's top and the body's top to fit the Popover - And there is enough space between the anchor's bottom and the body's bottom to fit the Popover - When the anchor is clicked - Then the popover is rendered below the anchor - And the horizontal center of the popover is aligned with the horizontal center of the anchor - - Scenario: Adjusted width - Given there is not enough space between the anchor's top and the body's top to fit the Popover - And there is not enough space between the anchor's bottom and the body's bottom to fit the Popover - When the anchor is clicked - Then the popover is rendered above the anchor - And the horizontal center of the popover is aligned with the horizontal center of the anchor - And the popover width is reduced to fit in the available space - - #!!Note: this is a exact copy of tooltip positioning diff --git a/cypress/drafts/Select/position.feature b/cypress/drafts/Select/position.feature deleted file mode 100644 index b0a487a0e1..0000000000 --- a/cypress/drafts/Select/position.feature +++ /dev/null @@ -1,25 +0,0 @@ -Feature: Position of select menu dropdown - - Background: - Given the select menu dropdown has a height of 368px - And the select menu dropdown has a width of 280px - - Scenario: Default rendering - Given there is enough space between the anchor's bottom and the body's bottom to fit the Select's menu - When the menu is opened - Then it is rendered below the select - And the left of the select is aligned with the left of the anchor - - Scenario: Flipped rendering when insufficient space below - Given there is not enough space between the anchor's bottom and the body's bottom to fit the Select's menu - And there is enough space between the anchor's top and the body's top to fit the Select's menu - When the menu is opened - Then it is rendered above the select - And the left of the select is aligned with the left of the anchor - - Scenario: A select with less than 368px available space below and above - Given there is not enough space between the anchor's bottom and the body's bottom to fit the Select's menu - And there is not enough space between the anchor's top and the body's top to fit the Select's menu - When the menu is opened - Then it is rendered below the select - And the height of the select dropdown menu is reduced to fit within the element diff --git a/cypress/drafts/Tooltip/position.feature b/cypress/drafts/Tooltip/position.feature deleted file mode 100644 index 0c60451986..0000000000 --- a/cypress/drafts/Tooltip/position.feature +++ /dev/null @@ -1,38 +0,0 @@ -Feature: Tooltip positioning - - Background: - Given the tooltip has a width of 300px and height of 28px - - Scenario: Spacing between anchor and tooltip - When the anchor is clicked - Then there is some space between the anchor and the tooltip - - Scenario: Default positioning - Given there is enough space between the anchor's top and the body's top to fit the Tooltip - When the anchor is clicked - Then the tooltip is rendered above the the anchor - And the horizontal center of the tooltip is aligned with the horizontal center of the anchor - - Scenario: Flipped vertical - Given there is not enough space between the anchor's top and the body's top to fit the Tooltip - And there is enough space between the anchor's bottom and the body's bottom to fit the Tooltip - When the anchor is clicked - Then the tooltip is rendered below the anchor - And the horizontal center of the tooltip is aligned with the horizontal center of the anchor - - Scenario: Adjusted horiztonal position - Given there is enough space between the anchor's top and the body's top to fit the Tooltip - And there is enough space between the body's left and the body's right to fit the Tooltip - And the Tooltip doesn't use more than 50% of the space between the body's sides and the anchor's sides - When the anchor is clicked - Then the tooltip is rendered above the anchor - And the horizontal center of the tooltip is aligned with the horizontal center of the anchor - - Scenario: Adjusted width - Given there is enough space between the anchor's top and the body's top to fit the Tooltip - And there is enough space between the body's left and the body's right to fit the Tooltip - And the Tooltip does use more than 50% of the space between the body's sides and the anchor's sides - When the anchor is clicked - Then the tooltip is rendered above the anchor - And the horizontal center of the tooltip is aligned with the horizontal center of the anchor - But the tooltip's width is reducesd to only take 50% of the space available next to the anchor diff --git a/cypress/integration/Backdrop/accepts_children.feature b/cypress/integration/Backdrop/accepts_children.feature deleted file mode 100644 index 246f9e1a09..0000000000 --- a/cypress/integration/Backdrop/accepts_children.feature +++ /dev/null @@ -1,5 +0,0 @@ -Feature: The Backdrop renders children - - Scenario: A Backdrop with children - Given a Backdrop with children is rendered - Then the children are visible diff --git a/cypress/integration/Backdrop/accepts_children/index.js b/cypress/integration/Backdrop/accepts_children/index.js deleted file mode 100644 index 93fee4d32e..0000000000 --- a/cypress/integration/Backdrop/accepts_children/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import { Given, Then } from 'cypress-cucumber-preprocessor/steps' - -Given('a Backdrop with children is rendered', () => { - cy.visitStory('Backdrop', 'With children') - cy.get('[data-test="dhis2-uicore-backdrop"]').should('be.visible') -}) - -Then('the children are visible', () => { - cy.contains('I am a child').should('be.visible') -}) diff --git a/cypress/integration/Backdrop/is_clickable.feature b/cypress/integration/Backdrop/is_clickable.feature deleted file mode 100644 index 19cac5a086..0000000000 --- a/cypress/integration/Backdrop/is_clickable.feature +++ /dev/null @@ -1,6 +0,0 @@ -Feature: The Backdrop has an onClick api - - Scenario: The user clicks on the Backdrop - Given a Backdrop with onClick handler is rendered - When the user clicks on the Backdrop - Then the onClick handler will be called diff --git a/cypress/integration/Backdrop/is_clickable/index.js b/cypress/integration/Backdrop/is_clickable/index.js deleted file mode 100644 index 3b7c90406c..0000000000 --- a/cypress/integration/Backdrop/is_clickable/index.js +++ /dev/null @@ -1,15 +0,0 @@ -import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps' - -Given('a Backdrop with onClick handler is rendered', () => { - cy.visitStory('Backdrop', 'With onClick') -}) - -When('the user clicks on the Backdrop', () => { - cy.get('[data-test="dhis2-uicore-backdrop"]').click() -}) - -Then('the onClick handler will be called', () => { - cy.window().then(win => { - expect(win.onClick).to.be.calledWith({}) - }) -}) diff --git a/cypress/integration/ComponentCover/accepts_children/index.js b/cypress/integration/ComponentCover/accepts_children/index.js index 49754b4375..64712cb8c5 100644 --- a/cypress/integration/ComponentCover/accepts_children/index.js +++ b/cypress/integration/ComponentCover/accepts_children/index.js @@ -1,7 +1,7 @@ import { Given, Then } from 'cypress-cucumber-preprocessor/steps' Given('a ComponentCover with children is rendered', () => { - cy.visitStory('ComponentCover', 'With children') + cy.visitStory('ComponentCover', 'With Children') cy.get('[data-test="dhis2-uicore-componentcover"]').should('be.visible') }) diff --git a/cypress/integration/ComponentCover/click_behavior.feature b/cypress/integration/ComponentCover/click_behavior.feature new file mode 100644 index 0000000000..90d61e59ac --- /dev/null +++ b/cypress/integration/ComponentCover/click_behavior.feature @@ -0,0 +1,22 @@ +Feature: The ComponentCover has configurable click behaviour + + Scenario: A non-blocking ComponentCover + Given a ComponentCover with pointerEvents none and a button below it is rendered + When the user clicks the button + Then the onClick handler of the button is called + + Scenario: A blocking ComponentCover + Given a ComponentCover with a button below it is rendered + When the user clicks on the button coordinates + Then the onClick handler of the button is not called + + Scenario: A blocking ComponentCover with an onClick handler + Given a ComponentCover with a button in it is rendered + When the user clicks on the ComponentCover + Then the onClick handler of the ComponentCover is called + + Scenario: Clicks orginating from children are ignored + Given a ComponentCover with a button in it is rendered + When the user clicks the button + Then the onClick handler of the button is called + But the onClick handler of the ComponentCover is not called diff --git a/cypress/integration/ComponentCover/click_behavior/index.js b/cypress/integration/ComponentCover/click_behavior/index.js new file mode 100644 index 0000000000..21f5dce3b6 --- /dev/null +++ b/cypress/integration/ComponentCover/click_behavior/index.js @@ -0,0 +1,59 @@ +import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps' + +Given( + 'a ComponentCover with pointerEvents none and a button below it is rendered', + () => { + cy.visitStory('ComponentCover', 'Non Blocking') + } +) + +Given('a ComponentCover with a button below it is rendered', () => { + cy.visitStory('ComponentCover', 'Blocking') +}) + +Given('a ComponentCover with a button in it is rendered', () => { + cy.visitStory('ComponentCover', 'With Click Handler') +}) + +When('the user clicks the button', () => { + cy.get('button').click() +}) + +When('the user clicks on the ComponentCover', () => { + cy.get('[data-test="dhis2-uicore-componentcover"]').click() +}) + +When('the user clicks on the button coordinates', () => { + cy.getPositionsBySelectors('button').then(([rect]) => { + // Get button center coordinates + const buttonCenterX = rect.left + rect.width / 2 + const buttonCenterY = rect.top + rect.height / 2 + + // click body on the button center + cy.get('body').click(buttonCenterX, buttonCenterY) + }) +}) + +Then('the onClick handler of the button is called', () => { + cy.window().then(win => { + expect(win.onButtonClick).to.be.calledOnce + }) +}) + +Then('the onClick handler of the ComponentCover is called', () => { + cy.window().then(win => { + expect(win.onComponentCoverClick).to.be.calledOnce + }) +}) + +Then('the onClick handler of the button is not called', () => { + cy.window().then(win => { + expect(win.onButtonClick).to.have.callCount(0) + }) +}) + +Then('the onClick handler of the ComponentCover is not called', () => { + cy.window().then(win => { + expect(win.onComponentCoverClick).to.have.callCount(0) + }) +}) diff --git a/cypress/integration/DropdownButton/opens_a_dropdown.feature b/cypress/integration/DropdownButton/opens_a_dropdown.feature index 6d1224ec5b..017874532e 100644 --- a/cypress/integration/DropdownButton/opens_a_dropdown.feature +++ b/cypress/integration/DropdownButton/opens_a_dropdown.feature @@ -7,5 +7,5 @@ Feature: The DropdownButton renders a dropdown Scenario: The user closes the dropdown Given a DropdownButton with opened dropdown is rendered - When the Backdrop is clicked + When the user clicks the backdrop Layer Then the dropdown is not rendered diff --git a/cypress/integration/DropdownButton/opens_a_dropdown/index.js b/cypress/integration/DropdownButton/opens_a_dropdown/index.js index afe2881c6c..39a2c47253 100644 --- a/cypress/integration/DropdownButton/opens_a_dropdown/index.js +++ b/cypress/integration/DropdownButton/opens_a_dropdown/index.js @@ -12,8 +12,8 @@ Given('a DropdownButton with opened dropdown is rendered', () => { cy.get('[data-test="dhis2-uicore-dropdownbutton-popper"]').should('exist') }) -When('the Backdrop is clicked', () => { - cy.get('[data-test="dhis2-uicore-backdrop"]').click() +When('the user clicks the backdrop Layer', () => { + cy.get('[data-test="dhis2-uicore-layer"]').click() }) Then('the dropdown is not rendered', () => { diff --git a/cypress/integration/Layer/accepts_children.feature b/cypress/integration/Layer/accepts_children.feature new file mode 100644 index 0000000000..5ffe48e8a3 --- /dev/null +++ b/cypress/integration/Layer/accepts_children.feature @@ -0,0 +1,5 @@ +Feature: The Layer renders children + + Scenario: A Layer with children + Given a Layer with children is rendered + Then the children are visible diff --git a/cypress/integration/Layer/accepts_children/index.js b/cypress/integration/Layer/accepts_children/index.js new file mode 100644 index 0000000000..b687dc83d8 --- /dev/null +++ b/cypress/integration/Layer/accepts_children/index.js @@ -0,0 +1,8 @@ +import { Given, Then } from 'cypress-cucumber-preprocessor/steps' + +Given('a Layer with children is rendered', () => { + cy.visitStory('Layer', 'Default') +}) +Then('the children are visible', () => { + cy.contains('I am a child').should('be.visible') +}) diff --git a/cypress/integration/Layer/click_behavior.feature b/cypress/integration/Layer/click_behavior.feature new file mode 100644 index 0000000000..cef3686777 --- /dev/null +++ b/cypress/integration/Layer/click_behavior.feature @@ -0,0 +1,22 @@ +Feature: The Layer has configurable click behaviour + + Scenario: A non-blocking layer + Given a Layer with pointerEvents none and a button below it is rendered + When the user clicks the button + Then the onClick handler of the button is called + + Scenario: A blocking layer + Given a Layer with a button below it is rendered + When the user clicks on the button coordinates + Then the onClick handler of the button is not called + + Scenario: A blocking layer with an onClick handler + Given a Layer with a button in it is rendered + When the user clicks on the layer + Then the onClick handler of the layer is called + + Scenario: Clicks orginating from children are ignored + Given a Layer with a button in it is rendered + When the user clicks the button + Then the onClick handler of the button is called + But the onClick handler of the layer is not called diff --git a/cypress/integration/Layer/click_behavior/index.js b/cypress/integration/Layer/click_behavior/index.js new file mode 100644 index 0000000000..72e8e19dd1 --- /dev/null +++ b/cypress/integration/Layer/click_behavior/index.js @@ -0,0 +1,59 @@ +import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps' + +Given( + 'a Layer with pointerEvents none and a button below it is rendered', + () => { + cy.visitStory('Layer', 'Non Blocking') + } +) + +Given('a Layer with a button below it is rendered', () => { + cy.visitStory('Layer', 'Blocking') +}) + +Given('a Layer with a button in it is rendered', () => { + cy.visitStory('Layer', 'With Click Handler') +}) + +When('the user clicks the button', () => { + cy.get('button').click() +}) + +When('the user clicks on the layer', () => { + cy.get('[data-test="dhis2-uicore-layer"]').click() +}) + +When('the user clicks on the button coordinates', () => { + cy.getPositionsBySelectors('button').then(([rect]) => { + // Get button center coordinates + const buttonCenterX = rect.left + rect.width / 2 + const buttonCenterY = rect.top + rect.height / 2 + + // click body on the button center + cy.get('body').click(buttonCenterX, buttonCenterY) + }) +}) + +Then('the onClick handler of the button is called', () => { + cy.window().then(win => { + expect(win.onButtonClick).to.be.calledOnce + }) +}) + +Then('the onClick handler of the layer is called', () => { + cy.window().then(win => { + expect(win.onLayerClick).to.be.calledOnce + }) +}) + +Then('the onClick handler of the button is not called', () => { + cy.window().then(win => { + expect(win.onButtonClick).to.have.callCount(0) + }) +}) + +Then('the onClick handler of the layer is not called', () => { + cy.window().then(win => { + expect(win.onLayerClick).to.have.callCount(0) + }) +}) diff --git a/cypress/integration/Layer/stacking.feature b/cypress/integration/Layer/stacking.feature new file mode 100644 index 0000000000..64a25d01c1 --- /dev/null +++ b/cypress/integration/Layer/stacking.feature @@ -0,0 +1,34 @@ +Feature: Layers are stacked on top of each other + + Scenario: Equal sibling layers + Given two equal sibling Layers are rendered + Then the second layer is on top of the first layer + + Scenario: Inequal sibling layers + Given an alert, blocking, and applicatioTop Layer are rendered as siblings + Then the alert layer is on top + + # use zIndex stacking context + Scenario: Nesting Layer elements with lower levels + Given a blocking layer is rendered as the child of an alert layer + Then the blocking layer is on top + And the blocking layer is a child of the alert layer + + # avoid stacking context upper bound issue + Scenario: Appending nested Layers with higher levels to the body + Given an alert layer is rendered as the child of a blocking layer + Then the alert layer is on top + And the alert layer is a sibling of the blocking layer + + # verify that bug from previous implementation is not there + # that bug was as follows: + # nested layers top element zIndex = 1000 + 1 + 1 = 1002 + # sibling layer element zIndex = 1001 + # so layer level 1001 would be below the nested layer with level 1000 + Scenario: Levels are respected when nesting layers + Given a layer with level 1001 is a sibling of 3 nested layers with level 1000 + Then the layer with level 1001 is on top + + Scenario: Nested higher levels still end up on top + Given an applicatioTop layer with a nested alert layer with a blocking sibling + Then the alert layer is on top diff --git a/cypress/integration/Layer/stacking/index.js b/cypress/integration/Layer/stacking/index.js new file mode 100644 index 0000000000..24465c53c0 --- /dev/null +++ b/cypress/integration/Layer/stacking/index.js @@ -0,0 +1,70 @@ +import { Given, Then } from 'cypress-cucumber-preprocessor/steps' + +Given('two equal sibling Layers are rendered', () => { + cy.visitStory('Layer', 'Equal Siblings') +}) + +Given( + 'an alert, blocking, and applicatioTop Layer are rendered as siblings', + () => { + cy.visitStory('Layer', 'Inequal Siblings') + } +) + +Given('a blocking layer is rendered as the child of an alert layer', () => { + cy.visitStory('Layer', 'Nested Lower Levels') +}) + +Given('an alert layer is rendered as the child of a blocking layer', () => { + cy.visitStory('Layer', 'Nested Higher Levels') +}) + +Given( + 'a layer with level 1001 is a sibling of 3 nested layers with level 1000', + () => { + cy.visitStory('Layer', 'Levels Are Respected When Nesting') + } +) + +Given( + 'an applicatioTop layer with a nested alert layer with a blocking sibling', + () => { + cy.visitStory('Layer', 'Nested Higher Level Ends On Top') + } +) + +Then('the second layer is on top of the first layer', () => { + assertLayerIsOnTop('second') +}) + +Then('the alert layer is on top', () => { + assertLayerIsOnTop('alert') +}) + +Then('the layer with level 1001 is on top', () => { + assertLayerIsOnTop('1001') +}) + +Then('the blocking layer is on top', () => { + assertLayerIsOnTop('blocking') +}) + +Then('the blocking layer is a child of the alert layer', () => { + cy.get('[data-test="blocking"]') + .parent() + .should('have.data', 'test', 'alert') +}) + +Then('the alert layer is a sibling of the blocking layer', () => { + cy.get('[data-test="blocking"]') + .next() + .should('have.data', 'test', 'alert') +}) + +function assertLayerIsOnTop(layerName) { + cy.get('body').click() + cy.window().then(win => { + expect(win.onLayerClick).to.be.calledOnce + expect(win.onLayerClick).to.be.calledWith(layerName) + }) +} diff --git a/cypress/integration/LayerContext/layers.feature.future b/cypress/integration/LayerContext/layers.feature.future deleted file mode 100644 index e4074032f4..0000000000 --- a/cypress/integration/LayerContext/layers.feature.future +++ /dev/null @@ -1,35 +0,0 @@ -Feature: Layers - - Scenario: Base layer z-index calculation - Given a Layer component wraps a component - And the prop zIndexBase is "0" - Then the Layer renders with a z-index of "0" - - Scenario: Application top layer z-index calculation - Given a Layer component wraps a component - And the prop zIndexBase is "2000" - Then the Layer renders with a z-index of "2000" - - Scenario: Blocking layer z-index calculation - Given a Layer component wraps a component - And the prop zIndexBase is "3000" - Then the Layer renders with a z-index of "3000" - - Scenario: Alert layer z-index calculation - Given a Layer component wraps a component - And the prop zIndexBase is "9999" - Then the Layer renders with a z-index of "9999" - - Scenario: Nested layer z-index calculation - Given a Layer component is nested inside of another Layer component - And the outer Layer component has a higher "" - And the inner Layer component has a lower "" - Then the inner Layer renders with "" - - Example: Nested z-indexes - | zIndexOuter | zIndexInner | zIndexResult | - | 0 | 2000 | 2000 | - | 2000 | 0 | 2001 | - | 3000 | 2000 | 3001 | - | 3001 | 2000 | 3002 | - | 3002 | 3000 | 3003 | diff --git a/cypress/integration/Modal/can_be_closed/index.js b/cypress/integration/Modal/can_be_closed/index.js index 57769159ca..971a811338 100644 --- a/cypress/integration/Modal/can_be_closed/index.js +++ b/cypress/integration/Modal/can_be_closed/index.js @@ -5,7 +5,7 @@ Given('a Modal with onClose handler is rendered', () => { }) When('the Screencover is clicked', () => { - cy.get('[data-test="dhis2-uicore-screencover"]').click('topLeft') + cy.get('[data-test="dhis2-uicore-layer"]').click('topLeft') }) Then('the onClose handler is called', () => { diff --git a/cypress/integration/MultiSelect/accepts_blur_cb.feature b/cypress/integration/MultiSelect/accepts_blur_cb.feature index 8e49e0359c..3dba87e52e 100644 --- a/cypress/integration/MultiSelect/accepts_blur_cb.feature +++ b/cypress/integration/MultiSelect/accepts_blur_cb.feature @@ -4,5 +4,5 @@ Feature: Calls onBlur cb when blurred Given a MultiSelect with onBlur handler is rendered And the MultiSelect input is clicked And the MultiSelect has focus - When the user clicks the backdrop + When the user clicks the backdrop layer Then the onBlur handler is called diff --git a/cypress/integration/MultiSelect/allows_invalid_options/index.js b/cypress/integration/MultiSelect/allows_invalid_options/index.js index 368c67f625..3d7dec5ad0 100644 --- a/cypress/integration/MultiSelect/allows_invalid_options/index.js +++ b/cypress/integration/MultiSelect/allows_invalid_options/index.js @@ -10,10 +10,9 @@ Given('a MultiSelect with invalid filterable options is rendered', () => { }) When('the user enters a filter string', () => { - cy.get('[data-test="dhis2-uicore-multiselect-filterinput"]') - .click() - .focused() - .type('invalid') + cy.get('[data-test="dhis2-uicore-multiselect-filterinput"] input').type( + 'invalid' + ) }) Then('the invalid options are displayed', () => { diff --git a/cypress/integration/MultiSelect/allows_selecting.feature b/cypress/integration/MultiSelect/allows_selecting.feature index 18ecb3fbb8..b1ae04a133 100644 --- a/cypress/integration/MultiSelect/allows_selecting.feature +++ b/cypress/integration/MultiSelect/allows_selecting.feature @@ -34,3 +34,11 @@ Feature: Selecting options And the MultiSelect is open When the disabled option is clicked Then the onchange handler is not called + + Scenario: The user clicks an option twice to select and deselect it + Given a MultiSelect is rendered to which options can be added + And the MultiSelect is open + When an option is clicked + Then the clicked option is selected + When the selected option is clicked again + Then the previously selected option is deselected diff --git a/cypress/integration/MultiSelect/allows_selecting/index.js b/cypress/integration/MultiSelect/allows_selecting/index.js index 386e51d608..c0914b5b92 100644 --- a/cypress/integration/MultiSelect/allows_selecting/index.js +++ b/cypress/integration/MultiSelect/allows_selecting/index.js @@ -20,7 +20,7 @@ When('an option is clicked', () => { }) When('the selected option is clicked', () => { - cy.get('[data-test="dhis2-uicore-backdrop"]') + cy.get('[data-test="dhis2-uicore-layer"]') .contains('option one') .click() }) @@ -37,6 +37,12 @@ When('the disabled option is clicked', () => { cy.contains('disabled option').click() }) +When('the selected option is clicked again', () => { + cy.get('[data-test="dhis2-uicore-multiselectoption"] label') + .contains('option one') + .click() +}) + Then('the clicked option is selected', () => { cy.window().then(win => { expect(win.onChange).to.be.calledOnce @@ -67,3 +73,10 @@ Then('the onchange handler is not called', () => { expect(win.onChange).to.not.be.called }) }) + +Then('the previously selected option is deselected', () => { + cy.window().then(win => { + expect(win.onChange).to.be.calledTwice + expect(win.onChange).to.be.calledWith({ selected: [] }) + }) +}) diff --git a/cypress/integration/MultiSelect/can_be_empty/index.js b/cypress/integration/MultiSelect/can_be_empty/index.js index e26134aa25..28b8bbc310 100644 --- a/cypress/integration/MultiSelect/can_be_empty/index.js +++ b/cypress/integration/MultiSelect/can_be_empty/index.js @@ -14,7 +14,7 @@ Given('an empty MultiSelect with custom empty component is rendered', () => { }) Then('an empty menu is displayed', () => { - cy.get('[data-test="dhis2-uicore-backdrop"]').should('exist') + cy.get('[data-test="dhis2-uicore-layer"]').should('exist') }) Then('the custom empty text is displayed', () => { diff --git a/cypress/integration/MultiSelect/can_be_opened_and_closed.feature b/cypress/integration/MultiSelect/can_be_opened_and_closed.feature index c57ddeea32..95b6b78a64 100644 --- a/cypress/integration/MultiSelect/can_be_opened_and_closed.feature +++ b/cypress/integration/MultiSelect/can_be_opened_and_closed.feature @@ -27,10 +27,10 @@ Feature: Opening and closing the MultiSelect When the spacebar is pressed on the focused element Then the options are displayed - Scenario: The user clicks the backdrop to hide the options + Scenario: The user clicks the backdrop layer to hide the options Given a MultiSelect with options is rendered And the MultiSelect is open - When the user clicks the backdrop + When the user clicks the backdrop layer Then the options are not displayed Scenario: The user presses the escape key to hide the options diff --git a/cypress/integration/MultiSelect/common/index.js b/cypress/integration/MultiSelect/common/index.js index e0d0f1c5d3..31e02a5eab 100644 --- a/cypress/integration/MultiSelect/common/index.js +++ b/cypress/integration/MultiSelect/common/index.js @@ -11,6 +11,10 @@ Given( } ) +Given('a MultiSelect is rendered to which options can be added', () => { + cy.visitStory('MultiSelect', 'With options that can be added to the input') +}) + Given('the MultiSelect is open', () => { cy.get('[data-test="dhis2-uicore-select-input"]').click() @@ -23,8 +27,8 @@ When('the MultiSelect input is clicked', () => { cy.get('[data-test="dhis2-uicore-select-input"]').click() }) -When('the user clicks the backdrop', () => { - cy.get('[data-test="dhis2-uicore-backdrop"]').click() +When('the user clicks the backdrop layer', () => { + cy.get('[data-test="dhis2-uicore-layer"]').click() }) Then('the options are not displayed', () => { diff --git a/cypress/integration/MultiSelect/position.feature b/cypress/integration/MultiSelect/position.feature index aece3cbb9e..7ecce17bd6 100644 --- a/cypress/integration/MultiSelect/position.feature +++ b/cypress/integration/MultiSelect/position.feature @@ -4,23 +4,44 @@ Feature: Position of MultiSelect menu dropdown Given there is enough space below the anchor to fit the MultiSelect menu When the MultiSelect is clicked Then the top of the menu is aligned with the bottom of the input - And the left of the MultiSelect is aligned with the left of the anchor + And the left of the Menu is aligned with the left of the Input Scenario: Flipped rendering when insufficient space below Given there is not enough space below the anchor to fit the MultiSelect menu When the MultiSelect is clicked Then the bottom of the menu is aligned with the top of the input - And the left of the MultiSelect is aligned with the left of the anchor + And the left of the Menu is aligned with the left of the Input Scenario: Shifting into view when insufficient space below and above Given there is not enough space above or below the anchor to fit the MultiSelect menu When the MultiSelect is clicked Then it is rendered on top of the MultiSelect - And the left of the MultiSelect is aligned with the left of the anchor + And the left of the Menu is aligned with the left of the Input - Scenario: Staying in position during when the window is scrolled + Scenario: Staying in position when the window is scrolled Given there is enough space below the anchor to fit the MultiSelect menu When the MultiSelect is clicked And the window is scrolled down Then the top of the menu is aligned with the bottom of the input - And the left of the MultiSelect is aligned with the left of the anchor + And the left of the Menu is aligned with the left of the Input + + Scenario: Adusting the menu width when the window resizes + Given there is enough space below the anchor to fit the MultiSelect menu + When the MultiSelect is clicked + Then the left of the Menu is aligned with the left of the Input + And the Menu and the Input have an equal width + When the window is resized to a greater width + Then the left of the Menu is aligned with the left of the Input + And the Menu and the Input have an equal width + + Scenario: Repositioning the menu when the Input grows + Given a MultiSelect is rendered to which options can be added + And the input is empty + When the MultiSelect is clicked + Then the top of the menu is aligned with the bottom of the input + And the left of the Menu is aligned with the left of the Input + When an option is clicked + Then the Input grows in height + And the top of the menu is aligned with the bottom of the input + And the left of the Menu is aligned with the left of the Input + diff --git a/cypress/integration/MultiSelect/position/index.js b/cypress/integration/MultiSelect/position/index.js index 78570ad22b..bebc52c64e 100644 --- a/cypress/integration/MultiSelect/position/index.js +++ b/cypress/integration/MultiSelect/position/index.js @@ -1,3 +1,4 @@ +import '../common' import { Given, Then, When } from 'cypress-cucumber-preprocessor/steps' Given( @@ -29,11 +30,40 @@ When('the window is scrolled down', () => { cy.scrollTo(0, 800) }) +When('the window is resized to a greater width', () => { + waitForResizeObserver(() => { + cy.viewport(1200, 660) + }) +}) + +When('an option is clicked', () => { + waitForResizeObserver(() => { + cy.contains('option one').click() + }) +}) + +Then('the input is empty', () => { + cy.get('[data-test="dhis2-uicore-select-input"]').then($el => { + cy.wrap($el.outerHeight()).as('emptyInputHeight') + }) + cy.get('[data-test="dhis2-uicore-select-input"] .root').should('be.empty') +}) + +Then('the Input grows in height', () => { + cy.get('@emptyInputHeight').then(emptyInputHeight => { + cy.get('[data-test="dhis2-uicore-select-input"]').then($el => { + expect($el.outerHeight()).to.be.greaterThan(emptyInputHeight) + }) + }) +}) + Then('the top of the menu is aligned with the bottom of the input', () => { // This test is used by the window scroll scenario // so needs to take y into account for the anchor getAnchorAndMenuRects().then(([anchorRect, menuRect]) => { - expect(menuRect.top).to.equal(anchorRect.bottom - anchorRect.y) + expect(menuRect.top).to.equal( + anchorRect.y - anchorRect.top + anchorRect.height + ) }) }) @@ -50,14 +80,23 @@ Then('it is rendered on top of the MultiSelect', () => { }) }) -Then( - 'the left of the MultiSelect is aligned with the left of the anchor', - () => { - getAnchorAndMenuRects().then(([anchorRect, menuRect]) => { - expect(anchorRect.left).to.equal(menuRect.left) - }) - } -) +Then('the left of the Menu is aligned with the left of the Input', () => { + getAnchorAndMenuRects().then(([anchorRect, menuRect]) => { + expect(anchorRect.left).to.equal(menuRect.left) + }) +}) + +Then('the Menu and the Input have an equal width', () => { + cy.get('[data-test="dhis2-uicore-multiselect"] .root-input').then( + $input => { + cy.get('[data-test="dhis2-uicore-select-menu-menuwrapper"]').then( + $menu => { + expect($input.outerWidth()).to.equal($menu.outerWidth()) + } + ) + } + ) +}) function getAnchorAndMenuRects() { return cy.getPositionsBySelectors( @@ -65,3 +104,32 @@ function getAnchorAndMenuRects() { '[data-test="dhis2-uicore-select-menu-menuwrapper"]' ) } + +function waitForResizeObserver(callback) { + cy.window().then(() => { + const id = 'resize-observer-callback-executed' + const oldNode = document.getElementById(id) + + // Cleanup + if (oldNode) { + oldNode.parentNode.removeChild(oldNode) + } + + cy.get('[data-test="dhis2-uicore-select"]').then($el => { + const el = $el[0] + const observer = new ResizeObserver(() => { + // Create element to wait for when resizeObserver callback is executed + const newNode = document.createElement('div') + newNode.setAttribute('id', id) + el.parentNode.appendChild(newNode) + }) + + observer.observe(el) + + callback() + + // Wait for element and DOM redraw + return cy.get(`#${id}`).then(() => cy.wait(1)) + }) + }) +} diff --git a/cypress/integration/Popover/clicking_outside.feature b/cypress/integration/Popover/clicking_outside.feature new file mode 100644 index 0000000000..d957e5f6b3 --- /dev/null +++ b/cypress/integration/Popover/clicking_outside.feature @@ -0,0 +1,6 @@ +Feature: Popover clicking outside + + Scenario: Responds to a click outdside the Popover + Given a default Popper is rendered with an onClickOutside handler + When the user clicks outside of the Popover + Then the clickOutside handler is called \ No newline at end of file diff --git a/cypress/integration/Popover/clicking_outside/index.js b/cypress/integration/Popover/clicking_outside/index.js new file mode 100644 index 0000000000..008cffdecd --- /dev/null +++ b/cypress/integration/Popover/clicking_outside/index.js @@ -0,0 +1,16 @@ +import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps' + +Given('a default Popper is rendered with arrow set to true', () => { + cy.visitStory('Popover', 'Default') +}) +Given('a default Popper is rendered with an onClickOutside handler', () => { + cy.visitStory('Popover', 'With On Click Outside') +}) +When('the user clicks outside of the Popover', () => { + cy.get('[data-test="dhis2-uicore-layer"]').click() +}) +Then('the clickOutside handler is called', () => { + cy.window().then(win => { + expect(win.onClickOutside).to.be.calledOnce + }) +}) diff --git a/cypress/integration/Popover/conditional_arrow.feature b/cypress/integration/Popover/conditional_arrow.feature new file mode 100644 index 0000000000..00ed935cc2 --- /dev/null +++ b/cypress/integration/Popover/conditional_arrow.feature @@ -0,0 +1,9 @@ +Feature: Popover conditional arrow + + Scenario: With arrow + Given a default Popper is rendered with arrow set to true + Then there is an arrow element in the Popover + + Scenario: Without arrow + Given a Popover is rendered with the arrow prop set to false + Then there is no arrow element in the Popover \ No newline at end of file diff --git a/cypress/integration/Popover/position.feature b/cypress/integration/Popover/position.feature new file mode 100644 index 0000000000..44716de9bd --- /dev/null +++ b/cypress/integration/Popover/position.feature @@ -0,0 +1,35 @@ +Feature: Popover positioning + + Scenario: Spacing + Given there is sufficient space to place the Popover above the reference element + Then there is some space between the anchor and the popover + And the arrow is showing + + Scenario: Default positioning + Given there is sufficient space to place the Popover above the reference element + Then the popover is placed above the reference element + And the horizontal center of the popover is aligned with the horizontal center of the reference element + And the arrow is showing + + Scenario: Flipped vertical + Given there is not enough space between the reference element top and the body top to fit the Popover + And there is sufficient space between the reference element bottom and the body bottom to fit the Popover + Then the popover is placed below the reference element + And the horizontal center of the popover is aligned with the horizontal center of the reference element + And the arrow is showing + + Scenario: On top of the reference element + Given there is not enough space between the reference element top and the body top to fit the Popover + And there is not enough space between the reference element bottom and the body bottom to fit the Popover + But there is sufficient space between the bottom of the reference element and the bottom of the Popover to show the arrow + Then the popover is placed op top of the reference element + And the horizontal center of the popover is aligned with the horizontal center of the reference element + And the arrow is showing + + Scenario: Hiding the arrow + Given there is very little space between the top of the reference element and the top of the body + And there is not enough space between the reference element bottom and the body bottom to fit the Popover + And there is not enough space between the top of the reference element and the top of the Popover to show the arrow + Then the popover is placed op top of the reference element + And the horizontal center of the popover is aligned with the horizontal center of the reference element + And the arrow is hiding diff --git a/cypress/integration/Popover/position/index.js b/cypress/integration/Popover/position/index.js index e60d43c37e..921f845df8 100644 --- a/cypress/integration/Popover/position/index.js +++ b/cypress/integration/Popover/position/index.js @@ -1,5 +1,4 @@ import { Given, Then } from 'cypress-cucumber-preprocessor/steps' -import { ARROW_SIZE } from '../../../../src/Popover/Arrow' // Stories Given( @@ -50,9 +49,7 @@ Given( 'there is sufficient space between the bottom of the reference element and the bottom of the Popover to show the arrow', () => { getRefAndPopoverPositions().then(([refPos, popoverPos]) => { - expect(refPos.bottom).to.be.greaterThan( - popoverPos.bottom + ARROW_SIZE - ) + expect(refPos.bottom).to.be.greaterThan(popoverPos.bottom + 8) }) } ) @@ -103,7 +100,7 @@ Then('the popover is placed op top of the reference element', () => { Then('there is some space between the anchor and the popover', () => { getRefAndPopoverPositions().then(([refPos, popoverPos]) => { - expect(popoverPos.bottom + ARROW_SIZE).to.equal(refPos.top) + expect(popoverPos.bottom + 8).to.equal(refPos.top) }) }) diff --git a/cypress/integration/ScreenCover/accepts_children.feature b/cypress/integration/ScreenCover/accepts_children.feature deleted file mode 100644 index d5e1f48590..0000000000 --- a/cypress/integration/ScreenCover/accepts_children.feature +++ /dev/null @@ -1,5 +0,0 @@ -Feature: The ScreenCover renders children - - Scenario: A ScreenCover with children - Given a ScreenCover with children is rendered - Then the children are visible diff --git a/cypress/integration/ScreenCover/accepts_children/index.js b/cypress/integration/ScreenCover/accepts_children/index.js deleted file mode 100644 index 7bdad4cb19..0000000000 --- a/cypress/integration/ScreenCover/accepts_children/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import { Given, Then } from 'cypress-cucumber-preprocessor/steps' - -Given('a ScreenCover with children is rendered', () => { - cy.visitStory('ScreenCover', 'With children') - cy.get('[data-test="dhis2-uicore-screencover"]').should('be.visible') -}) - -Then('the children are visible', () => { - cy.contains('I am a child').should('be.visible') -}) diff --git a/cypress/integration/ScreenCover/is_clickable.feature b/cypress/integration/ScreenCover/is_clickable.feature deleted file mode 100644 index aff017e98f..0000000000 --- a/cypress/integration/ScreenCover/is_clickable.feature +++ /dev/null @@ -1,6 +0,0 @@ -Feature: The ScreenCover provides an onClick api - - Scenario: The user clicks on the ScreenCover - Given a Screencover with onClick handler is rendered - When the user clicks on the Screencover - Then the onClick handler will be called diff --git a/cypress/integration/ScreenCover/is_clickable/index.js b/cypress/integration/ScreenCover/is_clickable/index.js deleted file mode 100644 index bb82835039..0000000000 --- a/cypress/integration/ScreenCover/is_clickable/index.js +++ /dev/null @@ -1,15 +0,0 @@ -import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps' - -Given('a Screencover with onClick handler is rendered', () => { - cy.visitStory('Screencover', 'With onClick') -}) - -When('the user clicks on the Screencover', () => { - cy.get('[data-test="dhis2-uicore-screencover"]').click() -}) - -Then('the onClick handler will be called', () => { - cy.window().then(win => { - expect(win.onClick).to.be.calledWith({}) - }) -}) diff --git a/cypress/integration/SingleSelect/accepts_blur_cb.feature b/cypress/integration/SingleSelect/accepts_blur_cb.feature index 78ca8474f1..493b97b198 100644 --- a/cypress/integration/SingleSelect/accepts_blur_cb.feature +++ b/cypress/integration/SingleSelect/accepts_blur_cb.feature @@ -4,5 +4,5 @@ Feature: Calls onBlur cb when blurred Given a SingleSelect with onBlur handler is rendered And the SingleSelect input is clicked And the SingleSelect has focus - When the user clicks the backdrop + When the user clicks the backdrop layer Then the onBlur handler is called diff --git a/cypress/integration/SingleSelect/can_be_empty/index.js b/cypress/integration/SingleSelect/can_be_empty/index.js index ae094d229e..36b51bd49d 100644 --- a/cypress/integration/SingleSelect/can_be_empty/index.js +++ b/cypress/integration/SingleSelect/can_be_empty/index.js @@ -14,7 +14,7 @@ Given('an empty SingleSelect with custom empty component is rendered', () => { }) Then('an empty menu is displayed', () => { - cy.get('[data-test="dhis2-uicore-backdrop"]').should('exist') + cy.get('[data-test="dhis2-uicore-layer"]').should('exist') }) Then('the custom empty text is displayed', () => { diff --git a/cypress/integration/SingleSelect/can_be_opened_and_closed.feature b/cypress/integration/SingleSelect/can_be_opened_and_closed.feature index 30e2f72432..5b705652b0 100644 --- a/cypress/integration/SingleSelect/can_be_opened_and_closed.feature +++ b/cypress/integration/SingleSelect/can_be_opened_and_closed.feature @@ -27,10 +27,10 @@ Feature: Opening and closing the SingleSelect When the spacebar is pressed on the focused element Then the options are displayed - Scenario: The user clicks the backdrop to hide the options + Scenario: The user clicks the backdrop layer to hide the options Given a SingleSelect with options is rendered And the SingleSelect is open - When the user clicks the backdrop + When the user clicks the backdrop layer Then the options are not displayed Scenario: The user presses the escape key to hide the options diff --git a/cypress/integration/SingleSelect/common/index.js b/cypress/integration/SingleSelect/common/index.js index 501b26c7b8..a448377229 100644 --- a/cypress/integration/SingleSelect/common/index.js +++ b/cypress/integration/SingleSelect/common/index.js @@ -23,8 +23,8 @@ When('the SingleSelect input is clicked', () => { cy.get('[data-test="dhis2-uicore-select-input"]').click() }) -When('the user clicks the backdrop', () => { - cy.get('[data-test="dhis2-uicore-backdrop"]').click() +When('the user clicks the backdrop layer', () => { + cy.get('[data-test="dhis2-uicore-layer"]').click() }) Then('the options are not displayed', () => { diff --git a/cypress/integration/SplitButton/arrow_opens_menu.feature b/cypress/integration/SplitButton/arrow_opens_menu.feature index 77437306ac..1c487b574a 100644 --- a/cypress/integration/SplitButton/arrow_opens_menu.feature +++ b/cypress/integration/SplitButton/arrow_opens_menu.feature @@ -10,6 +10,6 @@ Feature: The SplitButton renders a Popper Scenario: The user closes the Popper Given a SplitButton is rendered And the SplitButton menu is open - When the Backdrop is clicked + When the user clicks the backdrop Layer Then the menu is not visible And the component is not visible diff --git a/cypress/integration/SplitButton/arrow_opens_menu/index.js b/cypress/integration/SplitButton/arrow_opens_menu/index.js index 08571fa88d..e53edb8c7c 100644 --- a/cypress/integration/SplitButton/arrow_opens_menu/index.js +++ b/cypress/integration/SplitButton/arrow_opens_menu/index.js @@ -16,8 +16,8 @@ Given('the SplitButton menu is closed', () => { ) }) -When('the Backdrop is clicked', () => { - cy.get('[data-test="dhis2-uicore-backdrop"]').click() +When('the user clicks the backdrop Layer', () => { + cy.get('[data-test="dhis2-uicore-layer"]').click() }) Then('the menu is not visible', () => { diff --git a/cypress/support/selectSelectNthOption.js b/cypress/support/selectSelectNthOption.js index 029a0983c7..b8d9a26762 100644 --- a/cypress/support/selectSelectNthOption.js +++ b/cypress/support/selectSelectNthOption.js @@ -2,10 +2,10 @@ function selectSelectNthOption(subject, index, closeMenu = false) { cy.wrap(subject) .find('label + div > .root > .root-input') .click() - cy.get(`.backdrop > div > div > div > div:nth-child(${index})`).click() + cy.get(`.layer > div > div > div > div:nth-child(${index})`).click() if (closeMenu) { - cy.get('.backdrop').click('topRight') // close menu + cy.get('.layer').click('topRight') // close menu } } diff --git a/packages/forms/src/formDecorator.js b/packages/forms/src/formDecorator.js index 317ca4f272..7b933f47ec 100644 --- a/packages/forms/src/formDecorator.js +++ b/packages/forms/src/formDecorator.js @@ -25,6 +25,7 @@ class FormWithSpyAndSubmit extends React.Component { componentDidMount() { window.updateCypressProps = this.updateCypressProps window.clearCypressProps = this.clearCypressProps + this.forceUpdate() } componentWillUnmount() { diff --git a/packages/widgets/package.json b/packages/widgets/package.json index ca9eb03a5b..ef728cae71 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -27,9 +27,10 @@ "@dhis2/d2-i18n": "^1", "@dhis2/prop-types": "^1.5", "@dhis2/ui-constants": "5.0.0-alpha.4", - "@dhis2/ui-icons": "5.0.0-alpha.4", "@dhis2/ui-core": "5.0.0-alpha.4", - "classnames": "^2.2.6" + "@dhis2/ui-icons": "5.0.0-alpha.4", + "classnames": "^2.2.6", + "resize-observer-polyfill": "^1.5.1" }, "files": [ "build" diff --git a/packages/widgets/src/Backdrop/Backdrop.js b/packages/widgets/src/Backdrop/Backdrop.js deleted file mode 100644 index 44192b455c..0000000000 --- a/packages/widgets/src/Backdrop/Backdrop.js +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react' -import propTypes from '@dhis2/prop-types' -import cx from 'classnames' - -import { Layer } from '../LayerContext/LayerContext.js' -;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 - -/** - * @module - * @private - * @param {Object} PropTypes - * @returns {React.Component} - */ -const Backdrop = ({ - onClick, - transparent, - children, - zIndex, - className, - dataTest, -}) => { - return ( - - {zIndexComputed => ( -
onClick && onClick({}, event)} - data-test={dataTest} - > -
{ - // stop events from bubbling up through the - // children to the backdrop click handler - e.stopPropagation() - }} - > - {children} -
- - - - -
- )} -
- ) -} - -Backdrop.defaultProps = { - dataTest: 'dhis2-uicore-backdrop', -} - -/** - * @typedef {Object} PropTypes - * @static - * @prop {function} onClick - * @prop {boolean} transparent - * @prop {Node} children - * @prop {number} zIndex - * @prop {string} className - * @prop {string} [dataTest] - */ -Backdrop.propTypes = { - children: propTypes.node, - className: propTypes.string, - dataTest: propTypes.string, - transparent: propTypes.bool, - zIndex: propTypes.number, - onClick: propTypes.func, -} - -export { Backdrop } diff --git a/packages/widgets/src/Backdrop/Backdrop.stories.e2e.js b/packages/widgets/src/Backdrop/Backdrop.stories.e2e.js deleted file mode 100644 index fca5d2b922..0000000000 --- a/packages/widgets/src/Backdrop/Backdrop.stories.e2e.js +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react' -import { storiesOf } from '@storybook/react' -import { Backdrop } from './Backdrop.js' - -window.onClick = window.Cypress && window.Cypress.cy.stub() - -storiesOf('Backdrop', module) - .add('With onClick', () => ) - .add('With children', () => ( - - I am a child - - )) diff --git a/packages/widgets/src/CenteredContent/CenteredContent.js b/packages/widgets/src/CenteredContent/CenteredContent.js new file mode 100644 index 0000000000..3ff12d7d21 --- /dev/null +++ b/packages/widgets/src/CenteredContent/CenteredContent.js @@ -0,0 +1,61 @@ +import cx from 'classnames' +import React, { forwardRef } from 'react' +import propTypes from '@dhis2/prop-types' +;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 + +/** + * @module + * @param {CenteredContent.PropTypes} props + * @returns {React.Component} + * @example import { CenteredContent } from @dhis2/ui-core + * @see Live demo: {@link /demo/?path=/story/component-widget-centeredcontent--default|Storybook} + */ +const CenteredContent = forwardRef( + ({ className, dataTest, children, position }, ref) => ( +
+ {children} + +
+ ) +) + +CenteredContent.displayName = 'CenteredContent' + +CenteredContent.defaultProps = { + dataTest: 'dhis2-uicore-centeredcontent', + position: 'middle', +} + +/** + * @typedef {Object} PropTypes + * @static + * @prop {string} [className] + * @prop {Node} [children] + * @prop {string} [dataTest=dhis2-uicore-centeredcontent] + * @prop {string} [position=middle] One of `top`, `middle`, `bottom` + */ +CenteredContent.propTypes = { + children: propTypes.node, + className: propTypes.string, + dataTest: propTypes.string, + position: propTypes.oneOf(['top', 'middle', 'bottom']), +} + +export { CenteredContent } diff --git a/packages/widgets/src/CenteredContent/CenteredContent.stories.js b/packages/widgets/src/CenteredContent/CenteredContent.stories.js new file mode 100644 index 0000000000..5b050bafc1 --- /dev/null +++ b/packages/widgets/src/CenteredContent/CenteredContent.stories.js @@ -0,0 +1,26 @@ +import React from 'react' + +import { CenteredContent } from './CenteredContent.js' + +export default { + title: 'Component/Widget/CenteredContent', + component: CenteredContent, +} + +export const Default = () => ( + + Center me + +) + +export const Top = () => ( + + Center me + +) + +export const Bottom = () => ( + + Center me + +) diff --git a/packages/widgets/src/ComponentCover/ComponentCover.js b/packages/widgets/src/ComponentCover/ComponentCover.js index e4a24ee559..efe1c65dcb 100644 --- a/packages/widgets/src/ComponentCover/ComponentCover.js +++ b/packages/widgets/src/ComponentCover/ComponentCover.js @@ -1,30 +1,48 @@ +import cx from 'classnames' import React from 'react' import propTypes from '@dhis2/prop-types' import { layers } from '@dhis2/ui-constants' ;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 +const createClickHandler = onClick => event => { + // don't respond to clicks that originated in the children + if (onClick && event.target === event.currentTarget) { + onClick({}, event) + } +} + /** * @module * @param {ComponentCover.PropTypes} props * @returns {React.Component} * @example import { ComponentCover } from @dhis2/ui-core - * @see Live demo: {@link /demo/?path=/story/componentcover--circularloader|Storybook} + * @see Live demo: {@link /demo/?path=/story/component-widget-componentcover--default|Storybook} */ -const ComponentCover = ({ children, className, dataTest }) => ( -
+const ComponentCover = ({ + children, + className, + dataTest, + onClick, + pointerEvents, + translucent, +}) => ( +
{children} @@ -33,6 +51,7 @@ const ComponentCover = ({ children, className, dataTest }) => ( ComponentCover.defaultProps = { dataTest: 'dhis2-uicore-componentcover', + pointerEvents: 'all', } /** @@ -40,12 +59,18 @@ ComponentCover.defaultProps = { * @static * @prop {string} [className] * @prop {Node} [children] - * @prop {string} [dataTest] + * @prop {string} [dataTest=dhis2-uicore-componentcover] + * @prop {string} [pointerEvents=all] - One of 'all', 'none' + * @prop {boolean} [translucent] - When true a semi-transparent background is added + * @prop {function} [onClick] */ ComponentCover.propTypes = { children: propTypes.node, className: propTypes.string, dataTest: propTypes.string, + pointerEvents: propTypes.oneOf(['all', 'none']), + translucent: propTypes.bool, + onClick: propTypes.func, } export { ComponentCover } diff --git a/packages/widgets/src/ComponentCover/ComponentCover.stories.e2e.js b/packages/widgets/src/ComponentCover/ComponentCover.stories.e2e.js index 2cf463ee8c..35ba018ffc 100644 --- a/packages/widgets/src/ComponentCover/ComponentCover.stories.e2e.js +++ b/packages/widgets/src/ComponentCover/ComponentCover.stories.e2e.js @@ -1,7 +1,51 @@ import React from 'react' -import { storiesOf } from '@storybook/react' import { ComponentCover } from './ComponentCover.js' -storiesOf('ComponentCover', module).add('With children', () => ( - I am a child -)) +window.onButtonClick = window.Cypress && window.Cypress.cy.stub() +window.onComponentCoverClick = window.Cypress && window.Cypress.cy.stub() + +export default { + title: 'ComponentCover', + component: ComponentCover, + decorators: [ + storyFn => ( +
+ {storyFn()} + +
+ ), + ], +} + +export const WithChildren = () => ( + +

I am a child

+
+) + +export const NonBlocking = () => ( + <> + + + +) + +export const Blocking = () => ( + <> + + + +) + +export const WithClickHandler = () => ( + + + +) diff --git a/packages/widgets/src/ComponentCover/ComponentCover.stories.js b/packages/widgets/src/ComponentCover/ComponentCover.stories.js index 48ef0f0ecf..ceafa488ee 100644 --- a/packages/widgets/src/ComponentCover/ComponentCover.stories.js +++ b/packages/widgets/src/ComponentCover/ComponentCover.stories.js @@ -1,29 +1,75 @@ import React from 'react' -import { storiesOf } from '@storybook/react' import { ComponentCover } from './ComponentCover.js' -import { CircularLoader, Card } from '../index.js' +import { CircularLoader, CenteredContent } from '../index.js' -storiesOf('Utility/ComponentCover', module) - .add('CircularLoader', () => ( -
- +export default { + title: 'Component/Widget/ComponentCover', + component: ComponentCover, + decorators: [ + storyFn => ( +
+ {storyFn()} + +
+ ), + ], +} + +export const Default = () => ( + <> + + +

Text behind the cover

+

Lorem ipsum

+ +) + +export const Translucent = () => ( + <> + + +

Text behind the cover

+

Lorem ipsum

+ +) + +export const WithClickHandler = () => ( + <> + alert('Cover was clicked')} /> + +

Text behind the cover

+

Lorem ipsum

+ +) + +export const NonBlocking = () => ( + <> + + +

Text behind the cover

+

+ You can still select this text because the cover has pointer-event: + none. +

+ +) + +export const WithCenteredContentCircularLoader = () => ( + <> + + - - -

Text behind the cover

-

Lorem ipsum

-
- )) - - .add('Modal', () => ( -
- -
- Some text. -
-
- -

Text behind the cover

-

Lorem ipsum

-
- )) + +
+ +

Text behind the cover

+

Lorem ipsum

+ +) diff --git a/packages/widgets/src/DropdownButton/DropdownButton.js b/packages/widgets/src/DropdownButton/DropdownButton.js index 5652926295..fd00b41d98 100644 --- a/packages/widgets/src/DropdownButton/DropdownButton.js +++ b/packages/widgets/src/DropdownButton/DropdownButton.js @@ -1,13 +1,12 @@ import propTypes from '@dhis2/prop-types' import React, { Component } from 'react' -import { createPortal } from 'react-dom' import { resolve } from 'styled-jsx/css' import { spacers } from '@dhis2/ui-constants' import { ArrowDown, ArrowUp } from '@dhis2/ui-icons' import { Button } from '../Button/Button.js' -import { Backdrop } from '../Backdrop/Backdrop.js' +import { Layer } from '../Layer/Layer.js' import { Popper } from '../Popper/Popper.js' import { sharedPropTypes } from '@dhis2/ui-constants' @@ -88,19 +87,17 @@ class DropdownButton extends Component { - {open && - createPortal( - - - {component} - - , - document.body - )} + {open && ( + + + {component} + + + )} {arrow.styles} +
, @@ -54,13 +90,31 @@ const Layer = ({ children, className, level, position }) => { Layer.defaultProps = { position: 'fixed', + dataTest: 'dhis2-uicore-layer', + pointerEvents: 'all', + level: layers.applicationTop, } +/** + * @typedef {Object} PropTypes + * @static + * @prop {string} [className] + * @prop {Node} [children] + * @prop {string} [dataTest=dhis2-uicore-layer] + * @prop {number} [level=layers.applicationTop] + * @prop {string} [pointerEvents=all] - One of 'all', 'none' + * @prop {boolean} [translucent] - When true a semi-transparent background is added + * @prop {function} [onClick] + */ Layer.propTypes = { children: propTypes.node, className: propTypes.string, + dataTest: propTypes.string, level: propTypes.number, - position: propTypes.oneOf(['absolute', 'relative', 'fixed']), + pointerEvents: propTypes.oneOf(['all', 'none']), + position: propTypes.oneOf(['absolute', 'fixed']), + translucent: propTypes.bool, + onClick: propTypes.func, } export { Layer } diff --git a/packages/widgets/src/Layer/Layer.stories.e2e.js b/packages/widgets/src/Layer/Layer.stories.e2e.js new file mode 100644 index 0000000000..eaf2976efa --- /dev/null +++ b/packages/widgets/src/Layer/Layer.stories.e2e.js @@ -0,0 +1,114 @@ +import React from 'react' +import { layers } from '@dhis2/ui-constants' + +import { Layer } from './Layer.js' + +window.onButtonClick = window.Cypress && window.Cypress.cy.stub() +window.onLayerClick = window.Cypress && window.Cypress.cy.stub() + +const createNamedLayerClick = name => () => { + window.onLayerClick(name) +} + +export default { title: 'Layer', component: Layer } + +export const Default = () => ( + +

I am a child

+
+) + +export const NonBlocking = () => ( + <> + + + +) + +export const Blocking = () => ( + <> + + + +) + +export const WithClickHandler = () => ( + + + +) + +export const EqualSiblings = () => ( + <> + + + +) + +export const InequalSiblings = () => ( + <> + + + + +) + +export const NestedLowerLevels = () => ( + + + +) + +export const NestedHigherLevels = () => ( + + + +) + +export const LevelsAreRespectedWhenNesting = () => ( + <> + + + + + + + +) + +export const NestedHigherLevelEndsOnTop = () => ( + <> + + + + + +) diff --git a/packages/widgets/src/Layer/Layer.stories.js b/packages/widgets/src/Layer/Layer.stories.js index 2715541188..168060a7af 100644 --- a/packages/widgets/src/Layer/Layer.stories.js +++ b/packages/widgets/src/Layer/Layer.stories.js @@ -1,136 +1,60 @@ import React from 'react' -import { storiesOf } from '@storybook/react' -import propTypes from '@dhis2/prop-types' -import { layers } from '@dhis2/ui-constants' - import { Layer } from './Layer.js' +import { CircularLoader, CenteredContent } from '../index.js' -const logger = event => { - event.stopPropagation() - console.log('I happened', event.currentTarget) +export default { + title: 'Component/Widget/Layer', + component: Layer, } -const DivInLayer = ({ stackLevel, className, children }) => ( - -
- {children} -
- -
+export const Default = () => ( + <> + + +

Text behind the layer

+

Lorem ipsum

+ ) -DivInLayer.propTypes = { - stackLevel: propTypes.number.isRequired, - children: propTypes.node, - className: propTypes.string, -} +export const Translucent = () => ( + <> + -storiesOf('Component/Widget/Layer', module).add( - 'Stacked layers (pure nesting)', - () => ( - <> - - A - - B - - C - - - - - D - - E - - F - - - - - - ) +

Text behind the layer

+

Lorem ipsum

+ ) -storiesOf('Component/Widget/Layer', module).add('Inverse nesting', () => ( +export const WithClickHandler = () => ( <> - - A - - B - - C - - - - - D - - E - - F - - - - + alert('layer was clicked')} /> + +

Text behind the layer

+

Lorem ipsum

-)) +) + +export const NonBlocking = () => ( + <> + + +

Text behind the layer

+

+ You can still select this text because the layer has pointer-event: + none. +

+ +) + +export const WithCenteredContentCircularLoader = () => ( + <> + + + + + + +

Text behind the layer

+

Lorem ipsum

+ +) diff --git a/packages/widgets/src/LayerContext/LayerContext.js b/packages/widgets/src/LayerContext/LayerContext.js deleted file mode 100644 index dd8a49f377..0000000000 --- a/packages/widgets/src/LayerContext/LayerContext.js +++ /dev/null @@ -1,62 +0,0 @@ -import React, { useContext } from 'react' - -import propTypes from '@dhis2/prop-types' -import { layers } from '@dhis2/ui-constants' - -const LayerContext = React.createContext(0) - -const getStackedLayer = (zIndex, context) => { - // Keep alert layer constant - if (zIndex === layers.alert) { - return layers.alert - } - - // Differentiate between a stacked blocking and applicationTop layer - const layerIncrement = zIndex === layers.blocking ? 2 : 1 - const layer = context + layerIncrement - - // stay within stack layer boundaries defined by the design system - // https://github.com/dhis2/design-system/blob/master/principles/spacing-alignment.md#stacking - if (layer >= layers.alert) { - return layers.alert - 1 - } - - return layer -} - -const useLayer = zIndex => { - const context = useContext(LayerContext) - - if (context) return getStackedLayer(zIndex, context) - - return zIndex -} - -/** - * @module - * @private - * @param {Layer.PropTypes} props - * @returns {React.Component} - */ -const Layer = ({ children, zIndex }) => { - const newLayer = useLayer(zIndex) - - return ( - - {children(newLayer)} - - ) -} - -/** - * @typedef {Object} PropTypes - * @static - * @prop {function} children - * @prop {number} zIndex - */ -Layer.propTypes = { - children: propTypes.func.isRequired, - zIndex: propTypes.number, -} - -export { Layer, useLayer } diff --git a/packages/widgets/src/LinearLoader/LinearLoader.stories.js b/packages/widgets/src/LinearLoader/LinearLoader.stories.js index f60b644d20..cbcdf758f5 100644 --- a/packages/widgets/src/LinearLoader/LinearLoader.stories.js +++ b/packages/widgets/src/LinearLoader/LinearLoader.stories.js @@ -1,6 +1,8 @@ import React from 'react' +import { layers } from '@dhis2/ui-constants' + import { LinearLoader } from './LinearLoader.js' -import { ScreenCover, ComponentCover } from '../index.js' +import { Layer, CenteredContent, ComponentCover } from '../index.js' export default { title: 'Component/Widget/LinearLoader', @@ -10,15 +12,19 @@ export default { export const Determinate = () => export const OverlayPage = () => ( - - - + + + + + ) export const OverlayComponent = () => (
- - + + + +
) diff --git a/packages/widgets/src/Modal/Modal.js b/packages/widgets/src/Modal/Modal.js index ec45f0552e..157d5703fc 100644 --- a/packages/widgets/src/Modal/Modal.js +++ b/packages/widgets/src/Modal/Modal.js @@ -1,13 +1,23 @@ -import { createPortal } from 'react-dom' +import cx from 'classnames' import React from 'react' import propTypes from '@dhis2/prop-types' +import { layers, spacers, spacersNum } from '@dhis2/ui-constants' +import { resolve } from 'styled-jsx/css' -import { ScreenCover } from '../ScreenCover/ScreenCover.js' +import { Layer } from '../Layer/Layer.js' +import { CenteredContent } from '../CenteredContent/CenteredContent.js' import { sharedPropTypes } from '@dhis2/ui-constants' -import { ModalCard } from './ModalCard.js' +import { Card } from '../Card/Card.js' ;('') // TODO: https://github.com/jsdoc/jsdoc/issues/1718 +const scrollBoxCard = resolve` + div { + display: flex; + flex-direction: column; + } +` + /** * @module * @param {Modal.PropTypes} props @@ -45,21 +55,40 @@ export const Modal = ({ className, position, dataTest, -}) => - createPortal( - -