PERF: improve touch, swipe, panning performance on mobile menus
---
* stop event propagation on swipe events: other touch events were stealing a huge amount of time here. Stop event
propagation when handling pan events.
* animate with [web animations api](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Using_the_Web_Animations_API)
* prefer translate3d to hint for gpu rendering.
* query document for elements only on start move event, not on subsequent move
events
* remove unused calculations for directioned velocity and distance: all swipe/pan elements function in x/y direction only.
* re-implement scroll locking behavior.
re-implemented scroll lock behavior
---
With stop event propagation, we need to re-implement scroll locking on menu swipes.
Previously, this was using onTouchMove which was costly.
We may now use styling with overflow-y:hidden to lock scroll behavior.
overflow:hidden on html/body elements is now supported by iOS as of 2022
https://bugs.webkit.org/show_bug.cgi?id=153852https://bugs.webkit.org/show_bug.cgi?id=220908
UX: improve swipe
---
Some improvements to get gestures and swipes feeling a little more polished.
This focuses on end gesture, and how we transfer it to a css animation to
complete a menu open/close action.
Multitouch: events may pan, scroll, and zoom - especially on iOS safari.
Cancelling the swipe event allows for a more pleasant zooming experience.
* ease-out on menus opening, linear on close
* calculate animation duration for opening and closing,
attempt to better transfer user swipe velocity to css animation.
* more timely close/open and cleanup from calculated animation timing.
* add animation to closing menus on cloak tap
* correctly animate menus with ease-in and ease-out
* add swipe cancel event on multitouch event
DEV
---
* lean on promises
js animations api gives us promises to listen to. Update test waiters
to use waitForPromise from @ember/test-waiters instead of reigster/unregister.
* convert swipe mixin to its own class.
Convert swipe callbacks to custom events on the element.
Move shared functions for max animation time and close logic to
new shared class.
swipe-events lib uses custom events to trigger callbacks, rather than assuming
implemented hard coded function from the mixin's base class. Custom events are
triggered from the bound element as swipestart, swipeend, swipe
Add shared convenience functions for swipe events so they can be more easily
shared.
A client receives an initial swipe event and can check some state to see if it
wants to handle the swipe event and if it doesn't, calling
`event.preventDefault();` will prevent `swipe` and `swipeend` events from firing
until another distinct swipestart event is fired. Swipe events will auto-cancel on multitouch.
The scroll lock has also exposed as its own utility class.
# Top level view
This PR is the first version of converting the search menu and its logic from (deprecated) widgets to glimmer components. The changes are hidden behind a group based feature flag. This will give us the ability to test the new implementation in a production setting before fully committing to the new search menu.
# What has changed
The majority of the logic from the widget implementation has been updated to fit within the context of a glimmer component, but it has not fundamentally changed. Instead of having a single widget - [search-menu.js](https://github.com/discourse/discourse/blob/main/app/assets/javascripts/discourse/app/widgets/search-menu.js) - that built the bulk of the search menu logic, we split the logic into (20+) bite size components. This greatly increases the readability and makes extending a component in the search menu much more straightforward.
That being said, certain pieces needed to be rewritten from scratch as they did not translate from widget -> glimmer, or there was a general code upgraded needed. There are a few of these changes worth noting:
### Search Service
**Search Term** -> In the widget implementation we had a overly complex way of managing the current search term. We tracked the search term across multiple different states (`term`, `opts.term`, `searchData.term`) causing headaches. This PR introduces a single source of truth:
```js
this.search.activeGlobalSearchTerm
```
This tracked value is available anywhere the `search` service is injected. In the case the search term should be needs to be updated you can call
```js
this.search.activeGlobalSearchTerm = "foo"
```
**event listeners** -> In the widget implementation we defined event listeners **only** on the search input to handle things such as
- keyboard navigation / shortcuts
- closing the search menu
- performing a search with "enter"
Having this in one place caused a lot of bloat in our logic as we had to handle multiple different cases in one location. Do _x_ if it is this element, but do _y_ if it is another. This PR updates the event listeners to be attached to individual components, allowing for a more fine tuned set of actions per element. To not duplicate logic across multiple components, we have condensed shared logic to actions on the search service to be reused. For example - `this.search.handleArrowUpOrDown` - to handle keyboard navigation.
### Search Context
We have unique logic based on the current search context (topic / tag / category / user / etc). This context is set within a models route file. We have updated the search service with a tracked value `searchContext` that can be utilized and updated from any component where the search service is injected.
```js
# before
this.searchService.set("searchContext", user.searchContext);
# after
this.searchService.searchContext = user.searchContext;
```
# Views
<img width="434" alt="Screenshot 2023-06-15 at 11 01 01 AM" src="https://github.com/discourse/discourse/assets/50783505/ef57e8e6-4e7b-4ba0-a770-8f2ed6310569">
<img width="418" alt="Screenshot 2023-06-15 at 11 04 11 AM" src="https://github.com/discourse/discourse/assets/50783505/2c1e0b38-d12c-4339-a1d5-04f0c1932b08">
<img width="413" alt="Screenshot 2023-06-15 at 11 04 34 AM" src="https://github.com/discourse/discourse/assets/50783505/b871d164-88cb-405e-9b78-d326a6f63686">
<img width="419" alt="Screenshot 2023-06-15 at 11 07 51 AM" src="https://github.com/discourse/discourse/assets/50783505/c7309a19-f541-47f4-94ef-10fa65658d8c">
<img width="424" alt="Screenshot 2023-06-15 at 11 04 48 AM" src="https://github.com/discourse/discourse/assets/50783505/f3dba06e-b029-431c-b3d0-36727b9e6dce">
<img width="415" alt="Screenshot 2023-06-15 at 11 08 57 AM" src="https://github.com/discourse/discourse/assets/50783505/ad4e7250-040c-4d06-bf06-99652f4c7b7c">
Tested with Chrome <100, Firefox <100 and Samsung Internet 20, using
percentage-based height here works better for these browsers.
This was especially a problem for the Samsung Internet browser, because
it previously was hiding the "Dismiss" button on the user profile menu.
Using Javascript to read and recalculate sizing is prone to causing 'forced reflows', which are very expensive, especially on slower devices. This PR refactors the slide-in menus so that all of the height calculation is done using CSS. This is made possible by the new dvh (dynamic view height) units and env(safe-area-inset-bottom), both of which are supported on all of our target browsers.
In tests on a moto g50, on a sidebar with 16 categories, 15 tags, and 2 chat channels, this improves the sidebar opening time by around 50ms (6%).
* UX: replace highlight vars in popup menu
* UX: replace highlight vars in autcomplete
* UX: replace highlight vars in menu-panel
* UX: update style guide
* UX: bulk replace highlight vars in various small appearances
* UX: handle long userstatus in menupanel
* UX: remove margin on userstatus emoji
* UX: change emoji sise of user status in DM creator
* FIX: user status overflow on chat index
This commit includes various accessibility improvements for the new user menu:
* Add `title` attributes to the user menu tabs
* Properly label lists (by adding `aria-labelledby` to `<ul>` elements) for screen readers
* Change the user menu structure so that the tabs come before the content panel in the DOM, but use CSS to reverse them visually.
Normally, changing the order of elements via CSS is bad for accessibility, but I believe this is one of the rare scenarios where it [makes sense](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Flexible_Box_Layout/Ordering_Flex_Items#use_cases_for_order). Prior to this change, if you want to reach the first notification item after you select a tab using the keyboard, you have to hit <kbd>ctrl</kbd>+<kbd>tab</kbd> because the notifications list is before the tabs list. However, with this change, <kbd>tab</kbd> will move you to the first item in the list after you select a tab using your keyboard.
* Aria-hide the unread notifications badge/count on the tabs because the `title` attribute on the tab indicates the unread count.
* Add some tests.
This commit adds the profile tab to the experimental user menu. We're adding it to the user menu because it contains links/buttons that are not available anywhere else. We may remove the tab again if we find better places for those links/buttons, but for now it'll stay.
For more context on the experimental user menu, see https://github.com/discourse/discourse/pull/17379.
Before this commit, we carried custom code and styles for the sidebar on
mobile. This meant the look and feel of bringing up the sidebar on
mobile was very different from the user menu resulting in a very
inconsistent experience on mobile. Also, we could not leverage on the
existing swipe to close support on mobile.
In this commit, we made it such that the sidebar dropdown is always
rendered on mobile and made the interaction with the dropdown more
consistent with the user menu. There is also more parity with the old
hamburger dropdown when the experimental sidebar is disabled.
Some of the changes in this commit are extracted from https://github.com/discourse/discourse/pull/17379.
The bookmarks tab in the new user menu is different from the other tabs in that it can display a mixture of notifications and bookmarks. When there are unread bookmark reminder notifications, the tab displays all of these notifications at the top and fills the remaining space in the menu with the rest of the bookmarks. The bubble/badge count on the bookmarks tab indicates how many unread bookmark reminder notifications there are.
On the technical aspect, since this commit introduces a new `bookmark-item` component, we've done some refactoring so that all 3 "item" components (`notification-item`, `reviewable-item` and the new `bookmark-item`) inherit from a base component and get identical HTML structure so they all look consistent.
Internal tickets: t70584 and t65045.
Prior to this commit, we had a default Glimmer component that was responsible for handling generic rendering of notifications in the user menu, and many notification types had a custom Glimmer component that inherited from the default component to customize how they were rendered. That implementation was less than ideal because it meant plugins would have to create Glimmer components to customize notification types added by them and that would make the surface area of the API too big.
This commit changes the implementation so there's only one Glimmer component for rendering notifications, and then notification types that need to be customized can create a regular JavaScript class - `renderDirector` in the code - that provides the Glimmer component with the content it should display. We also introduce an API for plugins to register a renderer for a notification type or override an existing one.
Some of the changes are partially extracted from https://github.com/discourse/discourse/pull/17379.
When the experimental Sidebar is enabled, the hamburger drop down is replaced by a sidebar drop down. A user is given the ability to dock and undock the sidebar depending on their personal preference.
Do also note that the experimental sidebar is well, considered experimental at this point so I do not intend for the features here to be perfect. What I aim to do here is to ship the changes fast so that it can be used internally by the team to provide feedback. Custom links added by plugins and dark mode toggle has not been implemented as part of this commit as I aim to tackle it in another commit.
Co-authored-by: awesomerobot <kris.aubuchon@discourse.org>
* DEV: replace swipe events to use translate rather than left/right
translate is better for animations. also use native css animations for opening
and closing.
* a11y: respect prefers reduced motion on mobile timeline
* DEV: reduce jquery usage
* DEV: add tests for menu swipe events
test is run in 50% zoom/transform which means offsets and x of touch events need to be halved
Refactor test window to use a transform rather than non-standard zoom property
Co-authored-by: Penar Musaraj <pmusaraj@gmail.com>
If the user has not been sent any messages, show a message in the quick access menu with an educational message. If the user can send private messages, also show a link to open the "new message" composer:
This also adds a general improvement to the quick-access-panel, to be able to show an `emptyStateWidget` instead of just a message if there is nothing to show in the panel, as well as initial general styles for empty state.
* Revert "Revert "A11Y: Switch tabs using the keyboard (#12241)" (#12260)"
This reverts commit 4c1e02d412.
* FIX: Make sure that the "menu-link" is present when a plugin adds a tab.
Other changes:
- We put the notification tab first using JS instead of CSS. It's important because of the tab number data attribute, which the keyboard navigation uses.
- We only set the button id from the attrs object if it's a tab. Otherwise, it conflicts with the topic footer button
* A11Y: Switch tabs using the keyboard
According to the WAI-ARIA Authoring Practices, tabs should be navigable using the left/right arrow keys.
Additionally, the screen reader couldn't correctly announce that a tab was selected when clicking the tab icon. To fix this, we made the SVG icon non-clickable and set the "aria-hidden" attribute to true.
* Handle navigation events using appEvents