Просмотр исходного кода

Merge pull request #7606 from weseek/fix/gw-7948-fix-dropdown-flickering

fix: AlignRight DropdownMenu flickering
Ryoji Shimizu 2 лет назад
Родитель
Сommit
be000f7779

+ 3 - 1
apps/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -1,5 +1,6 @@
 import React, { useState, useCallback, useEffect } from 'react';
 
+import { modifiersForRightAlign } from '@growi/ui/dist/utils';
 import { useTranslation } from 'next-i18next';
 import {
   Dropdown, DropdownMenu, DropdownToggle, DropdownItem,
@@ -248,9 +249,10 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
   return (
     <DropdownMenu
       data-testid="page-item-control-menu"
-      modifiers={{ preventOverflow: { boundariesElement: 'viewport' } }}
       right={alignRight}
+      modifiers={modifiersForRightAlign}
       container="body"
+      persist={!!alignRight}
       style={{ zIndex: 1055 }} /* make it larger than $zindex-modal of bootstrap */
     >
       {contents}

+ 5 - 2
apps/app/src/components/Page/CopyDropdown.jsx

@@ -118,8 +118,11 @@ const CopyDropdown = (props) => {
           <span id={dropdownToggleId}>{children}</span>
         </DropdownToggle>
 
-        <DropdownMenu positionFixed modifiers={{ preventOverflow: { boundariesElement: 'viewport' } }}>
-
+        <DropdownMenu
+          positionFixed
+          modifiers={{ preventOverflow: { boundariesElement: 'viewport' } }}
+          style={{ zIndex: 1016 }} /* zIndex: 1016 // larger than z-index value of grw-subnav-fixed-container in GrowiSubNavigationSwitcher.module.scss */
+        >
           <div className="d-flex align-items-center justify-content-between">
             <DropdownItem header className="px-3">
               { t('copy_to_clipboard.Copy to clipboard') }

+ 13 - 13
apps/app/src/components/SavePageControls.tsx

@@ -80,17 +80,17 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
     <div className="d-flex align-items-center form-inline flex-nowrap">
 
       {isAclEnabled
-          && (
-            <div className="mr-2">
-              <GrantSelector
-                grant={grant}
-                disabled={isRootPage}
-                grantGroupId={grantedGroup?.id}
-                grantGroupName={grantedGroup?.name}
-                onUpdateGrant={updateGrantHandler}
-              />
-            </div>
-          )
+        && (
+          <div className="mr-2">
+            <GrantSelector
+              grant={grant}
+              disabled={isRootPage}
+              grantGroupId={grantedGroup?.id}
+              grantGroupName={grantedGroup?.name}
+              onUpdateGrant={updateGrantHandler}
+            />
+          </div>
+        )
       }
 
       <UncontrolledButtonDropdown direction="up">
@@ -101,9 +101,9 @@ export const SavePageControls = (props: SavePageControlsProps): JSX.Element | nu
           onClick={save}
           disabled={isWaitingSaveProcessing}
         >
-          { isWaitingSaveProcessing && (
+          {isWaitingSaveProcessing && (
             <i className="fa fa-spinner fa-pulse mr-1"></i>
-          ) }
+          )}
           {labelSubmitButton}
         </Button>
         <DropdownToggle caret color="primary" disabled={isWaitingSaveProcessing} />

+ 27 - 9
apps/app/test/cypress/integration/20-basic-features/20-basic-features--access-to-page.spec.ts

@@ -263,11 +263,16 @@ context('Access to Template Editing Mode', () => {
     cy.visit('/Sandbox');
     cy.waitUntilSkeletonDisappear();
 
-    cy.get('#grw-subnav-container').within(() => {
-      cy.getByTestid('open-page-item-control-btn').click({force: true});
-      cy.getByTestid('open-page-template-modal-btn').click({force: true});
+    cy.waitUntil(() => {
+      // do
+      cy.get('#grw-subnav-container').within(() => {
+        cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
+      });
+      // wait until
+      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
     });
 
+    cy.getByTestid('open-page-template-modal-btn').filter(':visible').click({force: true});
     cy.getByTestid('page-template-modal').should('be.visible');
     cy.screenshot(`${ssPrefix}-open-page-template-modal`);
 
@@ -294,11 +299,16 @@ context('Access to Template Editing Mode', () => {
     cy.visit('/Sandbox');
     cy.waitUntilSkeletonDisappear();
 
-    cy.get('#grw-subnav-container').within(() => {
-      cy.getByTestid('open-page-item-control-btn').click({force: true});
-      cy.getByTestid('open-page-template-modal-btn').click({force: true});
+    cy.waitUntil(() => {
+      // do
+      cy.get('#grw-subnav-container').within(() => {
+        cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
+      });
+      // Wait until
+      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
     });
 
+    cy.getByTestid('open-page-template-modal-btn').filter(':visible').click({force: true});
     cy.getByTestid('page-template-modal').should('be.visible');
 
     cy.getByTestid('template-button-decendants').click(({force: true}))
@@ -323,10 +333,18 @@ context('Access to Template Editing Mode', () => {
   it('Template is applied to pages created from PageTree (template for descendants)', () => {
     // delete /Sandbox/_template
     cy.visit('/Sandbox/_template');
-    cy.get('#grw-subnav-container').within(() => {
-      cy.getByTestid('open-page-item-control-btn').click({force: true});
-      cy.getByTestid('open-page-delete-modal-btn').click({force: true});
+
+    cy.waitUntil(() => {
+      //do
+      cy.get('#grw-subnav-container').within(() => {
+        cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
+      });
+      // wait until
+      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
     });
+
+    cy.getByTestid('open-page-delete-modal-btn').filter(':visible').click({force: true});
+
     cy.getByTestid('page-delete-modal').should('be.visible').within(() => {
       cy.intercept('POST', '/_api/pages.remove').as('remove');
       cy.getByTestid('delete-page-button').click();

+ 26 - 9
apps/app/test/cypress/integration/20-basic-features/20-basic-features--use-tools.spec.ts

@@ -136,11 +136,17 @@ context('Modal for page operation', () => {
   it('Page Deletion and PutBack is executed successfully', { scrollBehavior: false }, () => {
     cy.visit('/Sandbox/Bootstrap4');
 
-    cy.get('#grw-subnav-container').within(() => {
-      cy.getByTestid('open-page-item-control-btn').click({force: true});
-      cy.getByTestid('open-page-delete-modal-btn').click({force: true});
+    cy.waitUntil(() => {
+      // do
+      cy.get('#grw-subnav-container').within(() => {
+        cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
+      });
+      //wait until
+      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
     });
 
+    cy.getByTestid('open-page-delete-modal-btn').filter(':visible').click({force: true});
+
     cy.getByTestid('page-delete-modal').should('be.visible').within(() => {
       cy.screenshot(`${ssPrefix}-delete-modal`);
       cy.getByTestid('delete-page-button').click();
@@ -163,11 +169,17 @@ context('Modal for page operation', () => {
     cy.visit('/Sandbox/Bootstrap4');
     cy.waitUntilSkeletonDisappear();
 
-    cy.get('#grw-subnav-container').within(() => {
-      cy.getByTestid('open-page-item-control-btn').click({force: true});
-      cy.getByTestid('open-page-duplicate-modal-btn').click({force: true});
+    cy.waitUntil(() => {
+      // do
+      cy.get('#grw-subnav-container').within(() => {
+        cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
+      });
+      // wait until
+      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
     });
 
+    cy.getByTestid('open-page-duplicate-modal-btn').filter(':visible').click({force: true});
+
     cy.getByTestid('page-duplicate-modal').should('be.visible').screenshot(`${ssPrefix}-duplicate-bootstrap4`);
   });
 
@@ -175,11 +187,16 @@ context('Modal for page operation', () => {
     cy.visit('/Sandbox/Bootstrap4');
     cy.waitUntilSkeletonDisappear();
 
-    cy.get('#grw-subnav-container').within(() => {
-      cy.getByTestid('open-page-item-control-btn').click({force: true});
-      cy.getByTestid('open-page-move-rename-modal-btn').click({force: true});
+    cy.waitUntil(() => {
+      // do
+      cy.get('#grw-subnav-container').within(() => {
+        cy.getByTestid('open-page-item-control-btn').find('button').click({force: true});
+      });
+      // wait until
+      return cy.getByTestid('page-item-control-menu').then($elem => $elem.is(':visible'))
     });
 
+    cy.getByTestid('open-page-move-rename-modal-btn').filter(':visible').click({force: true});
     cy.getByTestid('grw-page-rename-button').should('be.disabled');
 
     cy.getByTestid('page-rename-modal').should('be.visible').screenshot(`${ssPrefix}-rename-bootstrap4`);

+ 28 - 0
packages/ui/src/interfaces/popper-data.ts

@@ -0,0 +1,28 @@
+interface Rect {
+  top: number
+  left: number
+  width: number
+  height: number
+}
+
+export interface PopperData {
+  styles: Partial<CSSStyleDeclaration>;
+  offsets: {
+    popper: Rect;
+    reference: Rect;
+    arrow: { top: number; left: number };
+  };
+}
+
+export interface Modifiers {
+  applyStyle: {
+    enabled: boolean
+  }
+  computeStyle: {
+    enabled: boolean,
+    fn: (data: PopperData) => PopperData
+  }
+  preventOverflow: {
+    boundariesElement: string
+  }
+}

+ 1 - 0
packages/ui/src/utils/index.ts

@@ -1,2 +1,3 @@
 export * from './browser-utils';
 export * from './use-fullscreen';
+export * from './reactstrap-modifiers';

+ 26 - 0
packages/ui/src/utils/reactstrap-modifiers.ts

@@ -0,0 +1,26 @@
+import { PopperData, Modifiers } from '~/interfaces/popper-data';
+
+// Conditional modifiers
+// To prevent flickering. only happened when `right` is true and persist props should be enabled
+export const modifiersForRightAlign: Modifiers = {
+  applyStyle: {
+    enabled: true,
+  },
+  computeStyle: {
+    enabled: true,
+    fn: (data: PopperData): PopperData => {
+      const popperRect = data.offsets.popper;
+      // Calculate transform styles
+      const newTransform = `translate3d(${popperRect.left - window.innerWidth + popperRect.width}px, ${popperRect.top}px, 0px)`;
+      const styles = {
+        top: '0px',
+        right: '0px',
+        willChange: 'transform',
+        transform: newTransform,
+      };
+      data.styles = { ...data.styles, ...styles };
+      return data;
+    },
+  },
+  preventOverflow: { boundariesElement: 'viewport' },
+};