Skip to content

Commit

Permalink
Display generated tags separately (#4291)
Browse files Browse the repository at this point in the history
* Display generated tags separately

Signed-off-by: Olga Bulat <[email protected]>

* Update snapshots

Signed-off-by: Olga Bulat <[email protected]>

* Fix collections test

Signed-off-by: Olga Bulat <[email protected]>

* Fix source/provider alignment

Signed-off-by: Olga Bulat <[email protected]>

* Add "What is this" link to generated tags

Signed-off-by: Olga Bulat <[email protected]>

* Update snapshots

Signed-off-by: Olga Bulat <[email protected]>

* Fix generated tags heading

Signed-off-by: Olga Bulat <[email protected]>

* Show "source tags" heading only if generated tags exist

Signed-off-by: Olga Bulat <[email protected]>

* Update snapshots

Signed-off-by: Olga Bulat <[email protected]>

* Replace the link text with "Learn more"

---------

Signed-off-by: Olga Bulat <[email protected]>
  • Loading branch information
obulat authored Jun 24, 2024
1 parent 78c90d5 commit 50fe56e
Show file tree
Hide file tree
Showing 32 changed files with 342 additions and 206 deletions.
229 changes: 229 additions & 0 deletions frontend/src/components/VMediaInfo/VCollapsibleTagSection.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
<template>
<div class="-my-1.5px">
<ul
ref="tagsContainerRef"
class="flex flex-wrap gap-3 overflow-y-hidden p-1.5px"
:class="heightClass"
>
<li v-for="tag in visibleTags" :key="tag">
<VTag :href="localizedTagPath(tag)">{{ tag }}</VTag>
</li>
</ul>
<VButton
v-if="hasOverflow"
size="small"
variant="transparent-tx"
has-icon-end
class="label-bold -ms-2 mt-4 hover:underline"
:aria-expanded="buttonStatus === 'show' ? 'false' : 'true'"
@click="handleClick"
>{{
$t(
buttonStatus === "show"
? "mediaDetails.tags.showMore"
: "mediaDetails.tags.showLess"
)
}}<VIcon
name="caret-down"
:size="4"
:class="{ '-scale-y-100 transform': buttonStatus === 'hide' }"
/></VButton>
</div>
</template>
<script lang="ts">
import {
computed,
defineComponent,
nextTick,
onMounted,
type PropType,
ref,
} from "vue"
import { useContext } from "@nuxtjs/composition-api"
import { useResizeObserver, watchDebounced } from "@vueuse/core"
import type { Tag } from "~/types/media"
import type { SupportedMediaType } from "~/constants/media"
import { useSearchStore } from "~/stores/search"
import { useI18n } from "~/composables/use-i18n"
import { focusElement } from "~/utils/focus-management"
import VTag from "~/components/VTag/VTag.vue"
import VButton from "~/components/VButton.vue"
import VIcon from "~/components/VIcon/VIcon.vue"
// The number of rows to display before collapsing the tags
const ROWS_TO_DISPLAY = 3
export default defineComponent({
name: "VCollapsibleTagSection",
components: { VIcon, VButton, VTag },
props: {
tags: {
type: Array as PropType<Tag[]>,
required: true,
},
mediaType: {
type: String as PropType<SupportedMediaType>,
required: true,
},
},
setup(props) {
const tagsContainerRef = ref<HTMLElement>()
const searchStore = useSearchStore()
const { $sendCustomEvent } = useContext()
const i18n = useI18n()
const localizedTagPath = (tag: string) => {
return searchStore.getCollectionPath({
type: props.mediaType,
collectionParams: { collection: "tag", tag },
})
}
const normalizedTags = computed(() => {
return Array.from(new Set(props.tags.map((tag) => tag.name)))
})
const collapsibleRowsStartAt = ref<number>()
const dir = computed(() => {
return i18n.localeProperties.dir
})
function isFurtherInline(previous: HTMLElement, current: HTMLElement) {
if (dir.value === "rtl") {
return previous.offsetLeft < current.offsetLeft
}
return previous.offsetLeft > current.offsetLeft
}
function findRowStartsAt(parent: HTMLElement) {
const children = Array.from(parent.children)
if (!children.length) {
return 0
}
let rowCount = 0
for (let i = 0; i < children.length; i++) {
const child = children[i] as HTMLElement
const previous = child.previousElementSibling as HTMLElement
if (!previous) {
rowCount++
} else if (isFurtherInline(previous, child)) {
rowCount++
}
if (rowCount === ROWS_TO_DISPLAY + 1) {
return i
}
}
return children.length
}
/**
* Only the first 3 rows of tags are visible by default.
* If we hide the tags using CSS only, they will be tabbable,
* even though they are not visible.
*/
const visibleTags = computed<string[]>(() => {
return collapsibleRowsStartAt.value && buttonStatus.value === "show"
? normalizedTags.value.slice(0, collapsibleRowsStartAt.value)
: normalizedTags.value
})
const hasOverflow = computed(() => {
return (
collapsibleRowsStartAt.value &&
collapsibleRowsStartAt.value < normalizedTags.value.length
)
})
onMounted(() => {
/**
* Find the index of the first item after the third row of tags. This is used
* to determine which tags to hide.
*/
if (tagsContainerRef.value) {
collapsibleRowsStartAt.value = findRowStartsAt(tagsContainerRef.value)
}
})
const buttonStatus = ref<"show" | "hide">("show")
/**
* Toggles the text for the "Show more" button. When showing more tags, we also
* focus the first tag in the newly-opened row for a11y.
*/
const handleClick = () => {
buttonStatus.value = buttonStatus.value === "show" ? "hide" : "show"
$sendCustomEvent("TOGGLE_TAG_EXPANSION", {
toState: buttonStatus.value === "show" ? "collapsed" : "expanded",
})
if (buttonStatus.value === "hide" && collapsibleRowsStartAt.value) {
nextTick(() => {
if (!collapsibleRowsStartAt.value) {
return
}
const firstTagInFourthRow = tagsContainerRef.value?.children.item(
collapsibleRowsStartAt.value
) as HTMLElement
focusElement(firstTagInFourthRow?.querySelector("a"))
})
}
}
const heightClass = computed(() => {
if (!hasOverflow.value) {
return "max-h-none"
}
/**
* Height is 3 rows of tags, gaps, and a padding for the focus rings.
* 3 * 2rem (tags) + 2 * 0.75rem (2 gaps) + 0.1875rem (margin for the focus ring)
*/
return buttonStatus.value === "show" ? "max-h-[7.6875rem]" : "mah-h-none"
})
const listWidth = ref<number>()
useResizeObserver(tagsContainerRef, (entries) => {
listWidth.value = entries[0].contentRect.width
})
watchDebounced(
listWidth,
(newWidth, oldWidth) => {
if (!tagsContainerRef.value) {
return
}
const isWidening = oldWidth && newWidth && newWidth > oldWidth
if (isWidening) {
collapsibleRowsStartAt.value = normalizedTags.value.length
}
nextTick(() => {
if (tagsContainerRef.value) {
collapsibleRowsStartAt.value = findRowStartsAt(
tagsContainerRef.value
)
}
})
},
{ debounce: 300 }
)
return {
tagsContainerRef,
localizedTagPath,
normalizedTags,
visibleTags,
hasOverflow,
buttonStatus,
heightClass,
handleClick,
}
},
})
</script>
18 changes: 13 additions & 5 deletions frontend/src/components/VMediaInfo/VMediaDetails.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<section class="flex flex-col gap-y-6">
<section class="flex flex-col gap-y-6 md:gap-y-8">
<header class="flex flex-row items-center justify-between">
<h2 class="heading-6 md:heading-5">
{{ $t(`mediaDetails.${media.frontendMediaType}Info`) }}
Expand All @@ -9,10 +9,18 @@
<div class="flex flex-col items-start gap-6 md:flex-row">
<slot name="thumbnail" />

<div class="flex w-full flex-grow flex-col gap-6">
<p v-if="media.description">{{ media.description }}</p>
<VMediaTags :tags="media.tags" :media-type="media.frontendMediaType" />
<VMetadata v-if="metadata" :metadata="metadata" />
<div class="flex flex-col gap-6 md:gap-8">
<div
class="flex w-full flex-grow flex-col items-start gap-6 md:flex-row"
>
<p v-if="media.description">{{ media.description }}</p>
<VMetadata v-if="metadata" :metadata="metadata" />
</div>
<VMediaTags
:tags="media.tags"
:media-type="media.frontendMediaType"
:provider="media.provider"
/>
</div>
</div>
</section>
Expand Down
Loading

0 comments on commit 50fe56e

Please sign in to comment.