Skip to content

Commit

Permalink
[full-ci] Implement long breadcrumb strategy (#8984)
Browse files Browse the repository at this point in the history
  • Loading branch information
lookacat authored May 24, 2023
1 parent fdf8f12 commit a9898e8
Show file tree
Hide file tree
Showing 11 changed files with 249 additions and 49 deletions.
6 changes: 6 additions & 0 deletions changelog/unreleased/enhancement-long-breadcrumb-strategy
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Long breadcrumb strategy

We've implemented a new solution to deal with long breadcrumbs even with long folder names.

https://github.com/owncloud/web/pull/8984
https://github.com/owncloud/web/issues/6731
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ describe('OcBreadcrumb', () => {
})
it('displays all items', () => {
const { wrapper } = getWrapper()
expect(wrapper.findAll('.oc-breadcrumb-list-item').length).toBe(items.length)
expect(wrapper.findAll('.oc-breadcrumb-list-item:not(.oc-invisible-sr)').length).toBe(
items.length
)
expect(wrapper.html()).toMatchSnapshot()
})
it('displays context menu trigger if enabled via property', () => {
Expand Down
161 changes: 148 additions & 13 deletions packages/design-system/src/components/OcBreadcrumb/OcBreadcrumb.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,28 @@
<nav :id="id" :class="`oc-breadcrumb oc-breadcrumb-${variation}`">
<ol class="oc-breadcrumb-list oc-flex oc-m-rm oc-p-rm">
<li
v-for="(item, index) in items"
v-for="(item, index) in displayItems"
:key="index"
class="oc-breadcrumb-list-item oc-flex oc-flex-middle"
:data-key="index"
:data-item-id="item.id"
:aria-hidden="item.isTruncationPlaceholder"
:class="[
'oc-breadcrumb-list-item',
'oc-flex',
'oc-flex-middle',
{
'oc-invisible-sr':
hiddenItems.indexOf(item) !== -1 ||
(item.isTruncationPlaceholder && hiddenItems.length === 0)
}
]"
>
<router-link v-if="item.to" :aria-current="getAriaCurrent(index)" :to="item.to">
<span>{{ item.text }}</span>
<router-link
v-if="item.to"
:aria-current="getAriaCurrent(index)"
:to="item.isTruncationPlaceholder ? lastHiddenItem.to : item.to"
>
<span class="oc-breadcrumb-item-text">{{ item.text }}</span>
</router-link>
<oc-icon
v-if="item.to"
Expand All @@ -20,12 +36,27 @@
v-else-if="item.onClick"
:aria-current="getAriaCurrent(index)"
appearance="raw"
class="oc-flex"
@click="item.onClick"
>
<span>{{ item.text }}</span>
<span
:class="[
'oc-breadcrumb-item-text',
{
'oc-breadcrumb-item-text-last': index === displayItems.length - 1
}
]"
v-text="item.text"
/>
</oc-button>
<span v-else :aria-current="getAriaCurrent(index)" tabindex="-1" v-text="item.text" />
<template v-if="showContextActions && index === items.length - 1">
<span
v-else
class="oc-breadcrumb-item-text"
:aria-current="getAriaCurrent(index)"
tabindex="-1"
v-text="item.text"
/>
<template v-if="showContextActions && index === displayItems.length - 1">
<oc-button
id="oc-breadcrumb-contextmenu-trigger"
v-oc-tooltip="contextMenuLabel"
Expand All @@ -48,7 +79,7 @@
</li>
</ol>
<oc-button
v-if="items.length > 1"
v-if="displayItems.length > 1"
appearance="raw"
type="router-link"
:aria-label="$gettext('Navigate one level up')"
Expand All @@ -58,13 +89,13 @@
<oc-icon name="arrow-left-s" fill-type="line" size="large" class="oc-mr-m" />
</oc-button>
</nav>
<div v-if="items.length > 1" class="oc-breadcrumb-mobile-current">
<div v-if="displayItems.length > 1" class="oc-breadcrumb-mobile-current">
<span class="oc-text-truncate" aria-current="page" v-text="currentFolder.text" />
</div>
</template>

<script lang="ts">
import { computed, defineComponent, PropType } from 'vue'
import { computed, defineComponent, nextTick, PropType, ref, unref, watch } from 'vue'
import { useGettext } from 'vue3-gettext'
import { AVAILABLE_SIZES } from '../../helpers/constants'
Expand Down Expand Up @@ -92,7 +123,6 @@ export default defineComponent({
OcIcon,
OcButton
},
props: {
/**
* Id for the breadcrumbs. If it's empty, a generated one will be used.
Expand Down Expand Up @@ -132,6 +162,26 @@ export default defineComponent({
return [...AVAILABLE_SIZES, 'remove'].some((e) => e === value)
}
},
/**
* Defines the maximum width of the breadcrumb. If the breadcrumb is wider than the given value, the breadcrumb
* will be reduced from the left side.
* If the value is -1, the breadcrumb will not be reduced.
*/
maxWidth: {
type: Number,
required: false,
default: -1
},
/**
* Defines the number of items that should be always displayed at the beginning of the breadcrumb.
* The default value is 2. e.g. Personal > ... > XYZ
*/
truncationOffset: {
type: Number,
required: false,
default: 2
},
/**
* Determines if the last breadcrumb item should have context menu actions.
*/
Expand All @@ -142,6 +192,65 @@ export default defineComponent({
},
setup(props) {
const { $gettext } = useGettext()
const visibleItems = ref<BreadcrumbItem[]>([])
const hiddenItems = ref<BreadcrumbItem[]>([])
const displayItems = ref<BreadcrumbItem[]>(props.items.slice())
const getBreadcrumbElement = (id): HTMLElement => {
return document.querySelector(`.oc-breadcrumb-list [data-item-id="${id}"]`)
}
const calculateTotalBreadcrumbWidth = () => {
let totalBreadcrumbWidth = 100 // 100px margin to the right to avoid breadcrumb from getting too close to the controls
visibleItems.value.forEach((item) => {
const breadcrumbElement = getBreadcrumbElement(item.id)
const itemClientWidth = breadcrumbElement?.getBoundingClientRect()?.width || 0
totalBreadcrumbWidth += itemClientWidth
})
return totalBreadcrumbWidth
}
const reduceBreadcrumb = (offsetIndex) => {
const breadcrumbMaxWidth = props.maxWidth
if (!breadcrumbMaxWidth) {
return
}
const totalBreadcrumbWidth = calculateTotalBreadcrumbWidth()
const isOverflowing = breadcrumbMaxWidth < totalBreadcrumbWidth
if (!isOverflowing || visibleItems.value.length <= props.truncationOffset + 1) {
return
}
// Remove from the left side
const removed = visibleItems.value.splice(offsetIndex, 1)
hiddenItems.value.push(removed[0])
reduceBreadcrumb(offsetIndex)
}
const lastHiddenItem = computed(() =>
hiddenItems.value.length >= 1 ? unref(hiddenItems)[unref(hiddenItems).length - 1] : { to: {} }
)
const renderBreadcrumb = () => {
displayItems.value = [...props.items]
if (displayItems.value.length > props.truncationOffset - 1) {
displayItems.value.splice(props.truncationOffset - 1, 0, {
text: '...',
allowContextActions: false,
to: {},
isTruncationPlaceholder: true
})
}
visibleItems.value = [...displayItems.value]
hiddenItems.value = []
nextTick(() => {
reduceBreadcrumb(props.truncationOffset)
})
}
watch([() => props.maxWidth, () => props.items], renderBreadcrumb, { immediate: true })
const currentFolder = computed<BreadcrumbItem>(() => {
if (props.items.length === 0 || !props.items) {
Expand All @@ -161,14 +270,34 @@ export default defineComponent({
return props.items.length - 1 === index ? 'page' : null
}
return { currentFolder, parentFolderTo, contextMenuLabel, getAriaCurrent }
return {
currentFolder,
parentFolderTo,
contextMenuLabel,
getAriaCurrent,
visibleItems,
hiddenItems,
renderBreadcrumb,
displayItems,
lastHiddenItem
}
}
})
</script>

<style lang="scss">
.oc-breadcrumb {
overflow: hidden;
&-item-text {
max-width: 200px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
&-last {
vertical-align: text-bottom;
}
}
&-mobile-current,
&-mobile-navigation {
Expand All @@ -184,7 +313,11 @@ export default defineComponent({
list-style: none;
align-items: baseline;
flex-wrap: wrap;
flex-wrap: nowrap;
span {
white-space: nowrap;
}
#oc-breadcrumb-contextmenu-trigger > span {
vertical-align: middle;
Expand Down Expand Up @@ -223,6 +356,8 @@ export default defineComponent({
font-size: var(--oc-font-size-medium);
color: var(--oc-color-text-default);
display: inline-block;
vertical-align: sub;
line-height: normal;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,37 @@
exports[`OcBreadcrumb displays all items 1`] = `
<nav class="oc-breadcrumb oc-breadcrumb-default" id="oc-breadcrumbs-2">
<ol class="oc-breadcrumb-list oc-flex oc-m-rm oc-p-rm">
<li class="oc-breadcrumb-list-item oc-flex oc-flex-middle">
<li class="oc-breadcrumb-list-item oc-flex oc-flex-middle" data-key="0">
<router-link-stub tag="a" to="[object Object]">
<span>First folder</span>
<span class="oc-breadcrumb-item-text">First folder</span>
</router-link-stub>
<oc-icon-stub accessiblelabel="" class="oc-mx-xs" color="var(--oc-color-text-default)" filltype="line" name="arrow-right-s" size="medium" type="span" variation="passive"></oc-icon-stub>
<!--v-if-->
</li>
<li class="oc-breadcrumb-list-item oc-flex oc-flex-middle">
<li aria-hidden="true" class="oc-breadcrumb-list-item oc-flex oc-flex-middle oc-invisible-sr" data-key="1">
<router-link-stub tag="a" to="[object Object]">
<span class="oc-breadcrumb-item-text">...</span>
</router-link-stub>
<oc-icon-stub accessiblelabel="" class="oc-mx-xs" color="var(--oc-color-text-default)" filltype="line" name="arrow-right-s" size="medium" type="span" variation="passive"></oc-icon-stub>
<!--v-if-->
<oc-button-stub appearance="raw" disabled="false" gapsize="medium" justifycontent="center" size="medium" submit="button" type="button" variation="passive">
<span>Subfolder</span>
</li>
<li class="oc-breadcrumb-list-item oc-flex oc-flex-middle" data-key="2">
<!--v-if-->
<oc-button-stub appearance="raw" class="oc-flex" disabled="false" gapsize="medium" justifycontent="center" size="medium" submit="button" type="button" variation="passive">
<span class="oc-breadcrumb-item-text">Subfolder</span>
</oc-button-stub>
<!--v-if-->
</li>
<li class="oc-breadcrumb-list-item oc-flex oc-flex-middle">
<router-link-stub tag="a" to="[object Object]">
<span>Deep</span>
<li class="oc-breadcrumb-list-item oc-flex oc-flex-middle" data-key="3">
<router-link-stub aria-current="page" tag="a" to="[object Object]">
<span class="oc-breadcrumb-item-text">Deep</span>
</router-link-stub>
<oc-icon-stub accessiblelabel="" class="oc-mx-xs" color="var(--oc-color-text-default)" filltype="line" name="arrow-right-s" size="medium" type="span" variation="passive"></oc-icon-stub>
<!--v-if-->
</li>
<li class="oc-breadcrumb-list-item oc-flex oc-flex-middle">
<li class="oc-breadcrumb-list-item oc-flex oc-flex-middle" data-key="4">
<!--v-if-->
<span aria-current="page" tabindex="-1">Deeper ellipsize in responsive mode</span>
<span class="oc-breadcrumb-item-text" tabindex="-1">Deeper ellipsize in responsive mode</span>
<!--v-if-->
</li>
</ol>
Expand All @@ -43,30 +50,37 @@ exports[`OcBreadcrumb displays all items 1`] = `
exports[`OcBreadcrumb sets correct variation 1`] = `
<nav class="oc-breadcrumb oc-breadcrumb-lead" id="oc-breadcrumbs-1">
<ol class="oc-breadcrumb-list oc-flex oc-m-rm oc-p-rm">
<li class="oc-breadcrumb-list-item oc-flex oc-flex-middle">
<li class="oc-breadcrumb-list-item oc-flex oc-flex-middle" data-key="0">
<router-link-stub tag="a" to="[object Object]">
<span>First folder</span>
<span class="oc-breadcrumb-item-text">First folder</span>
</router-link-stub>
<oc-icon-stub accessiblelabel="" class="oc-mx-xs" color="var(--oc-color-text-default)" filltype="line" name="arrow-right-s" size="medium" type="span" variation="passive"></oc-icon-stub>
<!--v-if-->
</li>
<li class="oc-breadcrumb-list-item oc-flex oc-flex-middle">
<li aria-hidden="true" class="oc-breadcrumb-list-item oc-flex oc-flex-middle oc-invisible-sr" data-key="1">
<router-link-stub tag="a" to="[object Object]">
<span class="oc-breadcrumb-item-text">...</span>
</router-link-stub>
<oc-icon-stub accessiblelabel="" class="oc-mx-xs" color="var(--oc-color-text-default)" filltype="line" name="arrow-right-s" size="medium" type="span" variation="passive"></oc-icon-stub>
<!--v-if-->
<oc-button-stub appearance="raw" disabled="false" gapsize="medium" justifycontent="center" size="medium" submit="button" type="button" variation="passive">
<span>Subfolder</span>
</li>
<li class="oc-breadcrumb-list-item oc-flex oc-flex-middle" data-key="2">
<!--v-if-->
<oc-button-stub appearance="raw" class="oc-flex" disabled="false" gapsize="medium" justifycontent="center" size="medium" submit="button" type="button" variation="passive">
<span class="oc-breadcrumb-item-text">Subfolder</span>
</oc-button-stub>
<!--v-if-->
</li>
<li class="oc-breadcrumb-list-item oc-flex oc-flex-middle">
<router-link-stub tag="a" to="[object Object]">
<span>Deep</span>
<li class="oc-breadcrumb-list-item oc-flex oc-flex-middle" data-key="3">
<router-link-stub aria-current="page" tag="a" to="[object Object]">
<span class="oc-breadcrumb-item-text">Deep</span>
</router-link-stub>
<oc-icon-stub accessiblelabel="" class="oc-mx-xs" color="var(--oc-color-text-default)" filltype="line" name="arrow-right-s" size="medium" type="span" variation="passive"></oc-icon-stub>
<!--v-if-->
</li>
<li class="oc-breadcrumb-list-item oc-flex oc-flex-middle">
<li class="oc-breadcrumb-list-item oc-flex oc-flex-middle" data-key="4">
<!--v-if-->
<span aria-current="page" tabindex="-1">Deeper ellipsize in responsive mode</span>
<span class="oc-breadcrumb-item-text" tabindex="-1">Deeper ellipsize in responsive mode</span>
<!--v-if-->
</li>
</ol>
Expand Down
2 changes: 2 additions & 0 deletions packages/design-system/src/components/OcBreadcrumb/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { RouteLocationRaw } from 'vue-router'

export interface BreadcrumbItem {
id?: string
text: string
to?: RouteLocationRaw
allowContextActions?: boolean
onClick?: () => void
isTruncationPlaceholder?: boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ exports[`Spaces view loading states should render spaces list after loading has
<div class="oc-width-expand oc-height-1-1 oc-position-relative" id="admin-settings-wrapper">
<div class="oc-app-bar oc-py-s" id="admin-settings-app-bar">
<div class="admin-settings-app-bar-controls oc-flex oc-flex-between oc-flex-middle">
<oc-breadcrumb-stub class="oc-flex oc-flex-middle" contextmenupadding="medium" id="admin-settings-breadcrumb" items="[object Object],[object Object]" showcontextactions="false" variation="default"></oc-breadcrumb-stub>
<oc-breadcrumb-stub class="oc-flex oc-flex-middle" contextmenupadding="medium" id="admin-settings-breadcrumb" items="[object Object],[object Object]" maxwidth="-1" showcontextactions="false" truncationoffset="2" variation="default"></oc-breadcrumb-stub>
<portal-target name="app.runtime.mobile.nav"></portal-target>
<div>
<button aria-label="Open sidebar to view details" class="oc-button oc-rounded oc-button-m oc-button-justify-content-center oc-button-gap-m oc-button-passive oc-button-passive-raw oc-my-s oc-p-xs" id="files-toggle-sidebar" type="button">
Expand Down
Loading

0 comments on commit a9898e8

Please sign in to comment.