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

Add hover cards #1067

Merged
merged 7 commits into from
Dec 11, 2024
Merged

Add hover cards #1067

merged 7 commits into from
Dec 11, 2024

Conversation

matthew-white
Copy link
Member

@matthew-white matthew-white commented Nov 26, 2024

This PR closes getodk/central#670.

What has been done to verify that this works as intended?

I tried things out locally. I've deployed this branch to the QA server, so you can check out the hover cards there.

I've also updated existing tests and written new tests.

Why is this the best possible solution? Were any other approaches considered?

There are a few moving parts:

  • The hoverCard object (accessible from container) manages the reactive state of the hover card. It's similar in that way to the alert object, which manages the reactive state of the alert. (The Alert component is what actually uses that state to render the alert.)
  • The useHoverCard() composable sets up event listeners on mouseenter and mouseleave that will modify the hoverCard object. It's also responsible for waiting the 350ms to show the hover card.
  • Whenever the hoverCard object is modified, the HoverCards component will react to it. HoverCards sends one or more requests for the requestData needed to render the hover card. Once the responses are received, it renders the correct hover card in a popover.
  • The requestData resources that HoverCards uses to send requests are created in useHoverCardResources(). The resources transform the response data. The resources don't need to be reactive, because once a hover card is shown, it won't change. The useHoverCardResources() composable makes it easier to test various hover card components, since it can be passed to testRequestData().
  • The actual content of the hover card varies by resource. There is a hover card component for each resource: HoverCardForm, HoverCardSubmission, HoverCardDataset, HoverCardEntity. They all use the HoverCard component, which lays out the structure and styles of the hover card.
  • What's calling useHoverCard() are new *Link components. There is one for each resource: FormLink, SubmissionLink, DatasetLink, EntityLink. These links are the elements that trigger the hover card to be shown.

I think all these pieces are necessary. I also like that the hoverCard object has parallels to the alert object, as well as modalData().

One goal I had was to keep useHoverCard() fairly flexible and not tightly couple it to the *Link components. I think we're going to use hover cards in other ways in the future (e.g., I think with infonav buttons: getodk/central#827).

How does this change affect users? Describe intentional changes to behavior and behavior that could have accidentally been affected by code changes. In other words, what are the regression risks?

This PR updates a lot existing links to use the new *Link components. I think regression testing will be useful as a check that each of those links is still working as expected. In many cases, there's also good test coverage of the links.

The thing that I feel the most unsure about is whether the hover card is always hidden when it should be. I think so, but that would be a good area for the QA team to explore. (I've noted that in a comment on the release criteria.)

Once we add infonav buttons, we will probably need to hide hover cards in more cases: see getodk/central#800. However, I think we can look into that in the future.

Before submitting this PR, please make sure you have:

  • run npm run test and npm run lint and confirmed all checks still pass OR confirm CircleCI build passes
  • verified that any code or assets from external sources are properly credited in comments or that everything is internally sourced

@matthew-white
Copy link
Member Author

I want to write a few more tests, especially of HoverCardEntity. However, I think there's more than enough to get started on code review. I'll go ahead and mark this PR as ready for review.

@matthew-white matthew-white marked this pull request as ready for review December 9, 2024 07:39
Copy link
Member Author

@matthew-white matthew-white left a comment

Choose a reason for hiding this comment

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

Explanatory comments to help with async review

'LIST_RENDERING',
'CONDITIONALS',
['CONDITIONALS', 'DEFINITION'],
Copy link
Member Author

Choose a reason for hiding this comment

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

This is because I want to be able to specify v-if before :is. I feel like v-if should generally be the very first thing.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree.

@@ -75,7 +74,7 @@ dl:not(.dl-horizontal) {
// down to the fact that .dl-horizontal uses a CSS grid, while other <dl>
// elements use flexbox.
.dl-horizontal {
$padding-between: 10px;
$padding-between: 12px;
Copy link
Member Author

Choose a reason for hiding this comment

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

This will have a minor effect on EntityUploadHeaderErrors, but I think that's OK.

const to = computed(() => datasetPath(props.projectId, props.name));

const link = ref(null);
useHoverCard(computed(() => link.value?.$el), 'dataset', () => props);
Copy link
Member Author

Choose a reason for hiding this comment

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

We need to pass the HTMLElement of the <router-link> to useHoverCard(). It looks like that's accessible via $el, though I don't see that documented in the Vue Router docs. Apparently $el is generally accessible from components that use the Options API. Components that use the Composition API can opt into providing access to the root element by using defineExpose().

@@ -14,9 +14,8 @@ except according to the terms contained in the LICENSE file.
<div class="row">
<div class="col-xs-6 dataset-name-wrap">
<div class="dataset-name text-overflow-ellipsis" v-tooltip.text>
<router-link v-if="!dataset.isNew" :to="datasetPath(projectId, dataset.name)" v-tooltip.text>
Copy link
Member Author

Choose a reason for hiding this comment

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

The parent element specifies v-tooltip.text, so I don't think this one needs to.

$border: 1px solid #fff;
$border-radius: 12px;

.popover:has(.hover-card) { border-radius: $border-radius; }
Copy link
Member Author

Choose a reason for hiding this comment

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

I had to fight to make the border radius visible on the hover card and spent a bit of time trying to get that to work. Adding border-radius to .popover seemed to be needed.

@@ -33,7 +33,7 @@ implementation of the Bootstrap plugins. We could also consider implementing our
own popover functionality, perhaps using Popper.
-->
<template>
<div class="popover-content-source">
<div class="popover-content-source" aria-hidden="true">
Copy link
Member Author

Choose a reason for hiding this comment

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

Because we are now displaying this off-screen rather than hiding it with display: none, we tell screen readers to ignore it.

@@ -84,7 +83,7 @@ export default {
const { tabPath, tabClass } = useTabs(datasetPath());
return {
project, dataset, ...resourceStates([project, dataset]),
projectPath, datasetPath, tabPath, tabClass, canRoute
Copy link
Member Author

Choose a reason for hiding this comment

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

I searched for all uses of datasetPath() to see where to add DatasetLink. I noticed that this component doesn't use datasetPath() in the template, so it doesn't need to return it from setup().

</template>
<template v-else-if="canLinkToSubmissions">
<router-link :to="submissionsPath.all">{{ form.nameOrId }}</router-link>
<form-link :form="form" :to="submissionsPath.all"/>
Copy link
Member Author

Choose a reason for hiding this comment

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

canRoute() and LinkIfCan don't always work that well on the homepage, which is why <router-link> + v-if was used here. For example, form pages will check the form resource, but the form resource isn't set on the homepage, so it will never fail validateData when checked from the homepage.

Here, the default value of the to prop is primaryFormPath(). However, given the behavior of canRoute() on the homepage, primaryFormPath() won't ever return a path to the submissions page. That's why we specify submissionsPath.all explicitly.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've wanted to make this change on other occasions as well. There are times when it's convenient to pass a template ref to useEventListener().

? meta.instanceName
: submission.__id;
})
if (meta == null || typeof meta !== 'object') return null;
Copy link
Member Author

Choose a reason for hiding this comment

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

I thought I might as well check that meta is an object as well.

Copy link
Contributor

@sadiqkhoja sadiqkhoja left a comment

Choose a reason for hiding this comment

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

I am still reviewing this PR, wanted to share my observation up till now:

1- Hover card should be 3-4px above the viewport: "If there isn’t room onscreen (without needing to scroll) to fit the height of the card, I should see the bottom of the card 3-4px above the bottom of the browser viewport" getodk/central#797

2- What do you think of caching the response of hover requests until user navigates to another page/route? right now every time I hover on a link a request is sent.

3- If I create a new Submission in a separate tab and then hover over Form link on the project overview page, it shows the correct number of Submission count but count is not updated on the FormList table.

None of the above are super critical to get this PR merged.

timeoutId = null;
}
};
useEventListener(anchorRef, 'mouseleave', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice to have: I wish if hover cards don't hide on mouse leaving the anchor link and entering into the hover card.

One way to achieve that is to append popover inside the anchor instead of 'body' (src/components/popover.vue:87).

Copy link
Member Author

Choose a reason for hiding this comment

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

I like this idea. If the user could enter the hover card, then we could show tooltips over truncated text. That said, the release criteria says the opposite:

I should not be able to hover on or interact with the infocard.

How about we discuss more in the meeting tomorrow?

Copy link
Member Author

Choose a reason for hiding this comment

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

We decided that we wouldn't make this change for v2024.3, but I'm going to file a follow-up issue about this.

@matthew-white
Copy link
Member Author

3- If I create a new Submission in a separate tab and then hover over Form link on the project overview page, it shows the correct number of Submission count but count is not updated on the FormList table.

I actually think this is sort of a nice thing. It's a signal that the page you're viewing is out of date. We could patch the one form, but it might not be the only stale data on the page.

For what it's worth, I've noticed that GitHub does something similar with their hover cards. For example:

  • Navigate to the list of open PRs.
  • Open a PR in a new tab.
  • Merge the PR.
  • In the list of PRs, hover over the PR. The hover card will show that the PR is merged. However, the PR will not change in the list itself.

2- What do you think of caching the response of hover requests until user navigates to another page/route? right now every time I hover on a link a request is sent.

I like this idea! I don't think I'll have time to do it before release, but I've filed an issue for it and slotted it for the next release: #1093.

Since I mentioned them already, I'll say that I also noticed that GitHub does cache their hover cards. For example:

  • Navigate to the list of open PRs.
  • Hover over a PR.
  • Open the PR in a new tab.
  • Merge the PR.
  • In the list of PRs, hover over the PR. The hover card will show that the PR is still open.

Copy link
Contributor

@sadiqkhoja sadiqkhoja left a comment

Choose a reason for hiding this comment

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

This is amazing 🎉

Liked the approach of calculating and positioning hover-card, it's not easy.

Test coverage is also good 👍

src/container/hover-card.js Outdated Show resolved Hide resolved
src/composables/hover-card.js Show resolved Hide resolved
src/request-data/hover-card.js Show resolved Hide resolved
Comment on lines +58 to +62
const idParts = computed(() => {
const id = submission.__id;
const match = id.match(/^((?:uuid:)?[0-9a-f]{8})[0-9a-f-]+([0-9a-f]{8})$/);
return match != null ? [match[1], '…', match[2]] : [id];
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Some unit tests for this would be helpful. We can add them later

Copy link
Member Author

Choose a reason for hiding this comment

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

When you say unit tests, are you thinking that we should move this logic into a utility function somewhere? I'd be happy to do that.

There are some tests of this behavior in test/components/hover-card/submission.spec.js. There are a couple of cases that I didn't test:

  • The case where the instance ID doesn't start with uuid:, in which case it should still be truncated.
  • The case where the instance ID is not a UUID, in which case idParts.value falls back to [id].

If I added tests of those cases in test/components/hover-card/submission.spec.js, would that be enough? Or should I create a utility function and test that separately? I'd be happy with either approach.

Thinking of the future, I bet that there will be other cases in which we want to truncate instance IDs in the middle like this. When that day comes, I'll probably move some of this into its own small component.

Copy link
Contributor

Choose a reason for hiding this comment

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

I find regex harder to read, seeing unit (or other) tests assure me that it is doing what it suppose to do + tests are kind of requirement documentation as well.


const width = ref('');
const el = ref(null);
const resize = () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd like this function to be cross-browser tested

Copy link
Member Author

Choose a reason for hiding this comment

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

Sounds good. 👍 I think it'd be difficult to explain some of this logic to the QA team, but I could test it out locally. I usually work in Chrome, but it'd be easy for me to test it in Firefox. I can leave a comment about the scenarios I try out: there are a few specific ones that I have in mind.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think QA team uses Firefox, they will report if they see something strange. And I try in Safari

src/components/popover.vue Show resolved Hide resolved
@matthew-white
Copy link
Member Author

I want to write a few more tests, especially of HoverCardEntity.

I still need to finish these tests. I'll file a follow-up issue about that.

@matthew-white matthew-white merged commit dbf6458 into master Dec 11, 2024
1 check passed
@matthew-white matthew-white deleted the hover-cards branch December 11, 2024 22:48
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.

Add hover cards
2 participants