Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: AppLayout horizontal scroll #2839

Merged
merged 16 commits into from
Oct 21, 2024
Merged

Conversation

georgylobko
Copy link
Contributor

Description

When 3 drawers are open on a page with the app layout toolbar, resizing the page eventually causes a horizontal scrollbar to appear (see the picture below), even though all drawers are resized to their minimum size. A solution for that is to check for the horizontal scroll on resize and close the drawers following the priority: navigation bar first, then the first opened drawer.

Screenshot 2024-10-09 at 19 08 30

Related links, issue #, if available: n/a

How has this been tested?

Review checklist

The following items are to be evaluated by the author(s) and the reviewer(s).

Correctness

  • Changes include appropriate documentation updates.
  • Changes are backward-compatible if not indicated, see CONTRIBUTING.md.
  • Changes do not include unsupported browser features, see CONTRIBUTING.md.
  • Changes were manually tested for accessibility, see accessibility guidelines.

Security

Testing

  • Changes are covered with new/existing unit tests?
  • Changes are covered with new/existing integration tests?

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Copy link

codecov bot commented Oct 10, 2024

Codecov Report

Attention: Patch coverage is 96.96970% with 1 line in your changes missing coverage. Please review.

Project coverage is 96.21%. Comparing base (a0670ee) to head (1f1ca5f).
Report is 17 commits behind head on main.

Files with missing lines Patch % Lines
src/app-layout/visual-refresh-toolbar/index.tsx 96.87% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2839   +/-   ##
=======================================
  Coverage   96.20%   96.21%           
=======================================
  Files         761      761           
  Lines       21447    21521   +74     
  Branches     6940     6968   +28     
=======================================
+ Hits        20633    20706   +73     
- Misses        806      807    +1     
  Partials        8        8           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@@ -154,7 +154,8 @@ describe('Visual refresh toolbar only', () => {
})
);

test('first opened drawer should be closed when active drawers can not be shrunk to accommodate it (1400px)', () => {
test(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized that this test suite didn't run earlier due to an extra wrap with an arrow function

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

@@ -227,6 +223,14 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef<AppLayoutProps.Ref, AppLa
const navigationFocusControl = useFocusControl(navigationOpen);
const splitPanelFocusControl = useSplitPanelFocusControl([splitPanelPreferences, splitPanelOpen]);

const onNavigationToggle = useCallback(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this handler below because it relies on navigationFocusControl, which is declared earlier

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use our useStableCallback util.

There is no promise that onNavigationChange is a stable callback, so your code does not really do what you expect.

To an extent, the same applies to other useCallback additions here. Make sure their dependencies are really stable (drawers array, for example is not)

@@ -77,7 +77,7 @@ describe.each(['classic', 'refresh', 'refresh-toolbar'] as const)('%s', theme =>
});

describe('Visual refresh toolbar only', () => {
function setupTest(testFn: (page: BasePageObject) => Promise<void>) {
function setupTest(testFn: (page: BasePageObject, browser: WebdriverIO.Browser) => Promise<void>) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This typing is not cross-version compatible. You will make Andrei's life upgrading webdriverio version easier, if you use a more portable version

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what exactly is not portable? the type WebdriverIO.Browser?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this type is not even imported here in this file

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because this type is globally available: https://webdriver.io/docs/api/globals/

what exactly do you want me to change to make it forward compatible?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return;
}
const throttledOnPageResize = throttle(onPageResize, 500);
window.addEventListener('resize', throttledOnPageResize);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might wonder why I didn't use useResizeObserver—the reason is that this approach is easier to mock and test

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a potential for future issues.

Also, this makes app layout depending on user interactions – open and then resize renders a different UI than opening on a small screen initially. We should not allow such deviations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is your suggestion here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do something similar to computeLayout function. The decision for drawers visibility should be a pure function too.

function setupTest(testFn: (page: BasePageObject) => Promise<void>) {
class PageObject extends BasePageObject {
getBrowser() {
return this.browser;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the browser property is protected in the BasePageObject class and it needs to be public here. get browser does not work as typescript considers it as overriding and throws TS2611

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is by design, to limit accidental use of the browser field directly and stick to high level methods.

For this task, you could introduce page.hasHorizontalScroll utility here.

If you think browser should not be private, we can simply expose it in browser test tools after discussion with our team

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed

@georgylobko georgylobko marked this pull request as ready for review October 11, 2024 09:54
@georgylobko georgylobko requested a review from a team as a code owner October 11, 2024 09:54
@georgylobko georgylobko requested review from abdhalees and just-boris and removed request for a team October 11, 2024 09:54
@@ -154,7 +154,8 @@ describe('Visual refresh toolbar only', () => {
})
);

test('first opened drawer should be closed when active drawers can not be shrunk to accommodate it (1400px)', () => {
test(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

return;
}
const throttledOnPageResize = throttle(onPageResize, 500);
window.addEventListener('resize', throttledOnPageResize);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a potential for future issues.

Also, this makes app layout depending on user interactions – open and then resize renders a different UI than opening on a small screen initially. We should not allow such deviations.

Comment on lines 196 to 197
const hasHorizontalScroll = () =>
page.getBrowser().execute(() => document.body.scrollWidth - document.body.clientWidth > 0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NB, we have another similar check here:

const [documentWidth, windowWidth] = await page.getViewportWidths();
expect(documentWidth).toEqual(windowWidth);

After merging this, we should follow up and unify detection logic. (Discussed it DM, your version is better, because it properly counts for the scrollbar widths)

expect(globalDrawersWrapper.findDrawerById('global-drawer-2')!.isActive()).toBe(true);

act(() => {
Object.defineProperty(HTMLElement.prototype, 'scrollWidth', {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. If we check only the document values, can we only override these values?
  2. Why do I not see a reset of this value after the test?

const totalActiveDrawersMinSize = getActiveDrawersTotalMinSize();
const activeNavigationWidth = navigationOpen ? navigationWidth : 0;
// collapsed content width is $space-layout-content-horizontal * 2 = 48px
const minContentVisibleWidth = 48;
Copy link
Contributor Author

@georgylobko georgylobko Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s no way to calculate this directly from the DOM tree since $space-layout-content-horizontal is applied in the grid-template to . area, specifically in this line: https://github.com/cloudscape-design/components/blob/main/src/app-layout/visual-refresh-toolbar/skeleton/styles.scss#L56

If you have a better idea, feel free to share it in this thread.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have this constant:

const contentPadding = 2 * 24; // space-xl

I suggest to reuse it somehow

@@ -116,27 +111,15 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef<AppLayoutProps.Ref, AppLa
// and compare a given number with the new drawer id min size

// the total size of all global drawers resized to their min size
let totalActiveDrawersMinSize = activeGlobalDrawersIds
.map(
activeDrawerId => combinedDrawers.find(drawer => drawer.id === activeDrawerId)?.defaultSize ?? MIN_DRAWER_SIZE
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized that this calculation works incorrectly, as it returns min size only if defaultSize is not defined, but it should compare them and return min of them. Fixed, see below.

Copy link
Member

@just-boris just-boris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good overall, only minor comments

@@ -227,6 +223,14 @@ const AppLayoutVisualRefreshToolbar = React.forwardRef<AppLayoutProps.Ref, AppLa
const navigationFocusControl = useFocusControl(navigationOpen);
const splitPanelFocusControl = useSplitPanelFocusControl([splitPanelPreferences, splitPanelOpen]);

const onNavigationToggle = useCallback(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use our useStableCallback util.

There is no promise that onNavigationChange is a stable callback, so your code does not really do what you expect.

To an extent, the same applies to other useCallback additions here. Make sure their dependencies are really stable (drawers array, for example is not)

const totalActiveDrawersMinSize = getActiveDrawersTotalMinSize();
const activeNavigationWidth = navigationOpen ? navigationWidth : 0;
// collapsed content width is $space-layout-content-horizontal * 2 = 48px
const minContentVisibleWidth = 48;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have this constant:

const contentPadding = 2 * 24; // space-xl

I suggest to reuse it somehow

return;
}

closeFirstDrawer();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't we want to have an iteration here, "close until hasHorizontalScroll is false"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we don't, because closing a drawer triggers a change in the dependency array, which leads to recalculations until the horizontal scrollbar is no longer present

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not instantly obvious, but okay

@georgylobko
Copy link
Contributor Author

#2839 (comment)

Tried. It leads to unexpected results

@georgylobko georgylobko added this pull request to the merge queue Oct 21, 2024
Merged via the queue into main with commit ec38d6e Oct 21, 2024
38 checks passed
@georgylobko georgylobko deleted the fix/app-layout-horizontal-scroll branch October 21, 2024 08:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants