Improve Breadcrumbs responsiveness [SCI-8494] (#5778)

- Refactor Breadcrumbs using VUE.
- Implement responsiveness breakpoints
This commit is contained in:
Soufiane 2023-07-25 09:24:49 +02:00 committed by GitHub
parent e7c238ab3d
commit effdc9563a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 280 additions and 135 deletions

View file

@ -1,23 +1,44 @@
.sci--layout-navigation-breadcrumbs {
--max-breadcrumbs-link-width: 11.25rem;
align-items: center;
background-color: $color-white;
column-gap: .875rem;
background-color: var(--sn-white);
display: flex;
flex-wrap: wrap;
flex-wrap: nowrap;
padding-left: 1.5rem;
}
.breadcrumbs-container {
@include font-small;
align-items: center;
display: flex;
flex-wrap: nowrap;
max-width: calc(100vw - var(--left-navigation-width) - 3rem);
overflow: hidden;
width: calc(100vw - var(--left-navigation-width) - 3rem);
.delimiter {
@include font-button;
color: var(--sn-grey);
font-weight: bold;
padding: 0 .5em;
}
}
.breadcrumbs-item {
align-items: center;
display: inline-flex;
flex-wrap: nowrap;
.breadcrumbs-link {
@include font-small;
color: var(--sn-blue);
display: inline-block;
max-width: var(--max-breadcrumbs-link-width);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:last-child {
color: var(--sn-grey);
&.shortened {
max-width: var(--max-breadcrumbs-link-width);
overflow: hidden;
text-overflow: ellipsis;
}
&.plain-text {
@ -25,24 +46,15 @@
}
}
.delimiter {
padding-bottom: .25rem;
&:hover {
text-decoration: none;
}
.breadcrumbs-collapsed-container {
color: var(--sn-blue);
position: relative;
.show-breadcrumbs {
align-items: center;
cursor: pointer;
display: flex;
position: relative;
}
a {
@include font-button;
color: var(--sn-blue);
}
&:last-child .breadcrumbs-link {
color: var(--sn-grey);
}
}
.breadcrumbs-collapsed-container .breadcrumbs-item .breadcrumbs-link {
color: var(--sn-blue);
}

View file

@ -1,42 +0,0 @@
.breadcrumbs-container {
@include font-small;
align-items: center;
display: flex;
flex-wrap: wrap;
.delimiter {
@include font-button;
color: $color-silver-chalice;
font-weight: bold;
padding: 0 .5em;
}
.breadcrumbs-link {
display: inline-block;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover {
text-decoration: none;
}
}
.breadcrumbs-collapsed-container {
color: $brand-primary;
position: relative;
.show-breadcrumbs {
align-items: center;
cursor: pointer;
display: flex;
position: relative;
}
a {
@include font-button;
color: $brand-primary;
}
}
}

View file

@ -0,0 +1,17 @@
import TurbolinksAdapter from 'vue-turbolinks';
import Vue from 'vue/dist/vue.esm';
import Breadcrumbs from '../../../vue/navigation/breadcrumbs/breadcrumbs.vue';
import PerfectScrollbar from 'vue2-perfect-scrollbar';
import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css';
Vue.use(TurbolinksAdapter);
Vue.use(PerfectScrollbar);
Vue.prototype.i18n = window.I18n;
window.breadcrumbsComponent = new Vue({
el: '#breadcrumbs',
name: 'BreadcrumbsContainer',
components: {
breadcrumbs: Breadcrumbs
}
});

View file

@ -0,0 +1,162 @@
<template>
<div class="breadcrumbs-container" ref="container">
<a
v-if="firstItem && firstItem.url !== lastItem.url"
:href="firstItem.url"
class="breadcrumbs-item"
:title="firstItem.label"
ref="firstItem"
>
<span
class="breadcrumbs-link"
:class="{
shortened:
state === State.SHORTENED || state === State.SHORTENED_COLLAPSED,
'plain-text': !firstItem.url
}"
>{{ firstItem.label }}</span>
<span class="delimiter">
<img :src="delimiterUrl" alt="navigate next" class="navigate_next" />
</span>
</a>
<template v-if="middleItems.length">
<BreadcrumbsDropdown
v-if="hiddenMiddleItems.length"
:items="hiddenMiddleItems"
:delimiterUrl="delimiterUrl"
/>
<a
v-for="(item, index) in visibleMiddleItems"
:key="item.url"
:href="item.url"
class="breadcrumbs-item"
:title="item.label"
:ref="`visibleMiddleItem-${index}`"
>
<span
class="breadcrumbs-link"
:class="{
shortened:
state === State.SHORTENED || state === State.SHORTENED_COLLAPSED
}"
>{{ item.label }}</span
>
<span class="delimiter">
<img :src="delimiterUrl" alt="navigate next" class="navigate_next" />
</span>
</a>
</template>
<span class="breadcrumbs-item" title="lastItem.label" ref="lastItem">
<span
class="breadcrumbs-link"
:title="lastItem.label"
:class="{
shortened:
state === State.SHORTENED || state === State.SHORTENED_COLLAPSED
}"
>{{ lastItem.label }}</span
>
</span>
</div>
</template>
<script>
import BreadcrumbsDropdown from "./breadcrumbs_dropdown.vue";
const State = Object.freeze({
INITIAL: "initial",
EXPENDED: "expended",
SHORTENED: "shortened",
SHORTENED_COLLAPSED: "shortened_collapsed"
});
const dropdownWidth = 60;
export default {
name: "Breadcrumbs",
props: {
breadcrumbsItems: String,
delimiterUrl: String
},
data() {
return {
dropdownWidth: dropdownWidth,
State,
state: State.INITIAL,
items: [],
hiddenMiddleItems: [],
visibleMiddleItems: []
};
},
components: {
BreadcrumbsDropdown
},
watch: {
breadcrumbsItems: {
immediate: true,
handler() {
this.items = JSON.parse(this.breadcrumbsItems);
this.reset();
this.$nextTick(() => {
this.updateItems();
});
}
}
},
computed: {
firstItem() {
if (this.items.length <= 1) {
return null;
}
return this.items[0];
},
lastItem() {
return this.items[this.items.length - 1];
},
middleItems() {
if (this.items.length <= 2) return [];
return this.items.slice(1, -1);
}
},
methods: {
updateItems() {
const width = this.$refs.container.clientWidth;
const scrollWidth = Array.from(this.$refs.container.children).reduce(
(totalWidth, child) => totalWidth + child.offsetWidth,
0
);
if (this.state === this.State.INITIAL) {
if (width < scrollWidth) {
this.state = this.State.SHORTENED;
this.$nextTick(() => {
this.updateItems();
});
} else {
this.state = this.State.EXPENDED;
}
} else if (this.state === this.State.SHORTENED) {
if (width < scrollWidth) {
this.state = this.State.SHORTENED_COLLAPSED;
let visibleWidth =
this.dropdownWidth +
this.$refs.firstItem.offsetWidth +
this.$refs.lastItem.offsetWidth;
let index = this.middleItems.length - 1;
while (visibleWidth <= width && index >= 0) {
visibleWidth += this.$refs[`visibleMiddleItem-${index}`][0]
.offsetWidth;
index--;
}
this.visibleMiddleItems = this.middleItems.slice(index + 2);
this.hiddenMiddleItems = this.middleItems.slice(0, index + 2);
}
}
},
reset() {
this.state = this.State.INITIAL;
this.hiddenMiddleItems = [];
this.visibleMiddleItems = this.middleItems;
}
}
};
</script>

View file

@ -0,0 +1,56 @@
<template>
<span class="breadcrumbs-collapsed-container">
<span
class="cursor-pointer text-sn-blue flex flex-nowrap whitespace-nowrap items-center gap-1"
data-toggle="dropdown"
:title="i18n.t('projects.index.breadcrumbs_collapsed')"
@click="open = !open"
>
<span class="caret"></span>
<span class="delimiter">
<img :src="delimiterUrl" alt="navigate next" class="navigate_next" />
</span>
</span>
<perfect-scrollbar>
<ul
class="absolute top-11 left-0 w-56 max-h-36 flex flex-col items-stretch gap-2 overflow-auto
bg-sn-white border border-sn-grey rounded-md shadow-md list-none p-2"
:class="{ hidden: !open }"
>
<li v-for="item in items" :key="item.url">
<a :href="item.url" class="breadcrumbs-item breadcrumb-link shortened" title="item.label">
<span
class="breadcrumbs-link shortened"
>{{ item.label }}</span>
<span class="delimiter">
<img
:src="delimiterUrl"
alt="navigate next"
class="navigate_next"
/>
</span>
</a>
</li>
</ul>
</perfect-scrollbar>
</span>
</template>
<script>
import { PerfectScrollbar } from 'vue2-perfect-scrollbar';
export default {
name: "BreadcrumbsDropdown",
props: {
items: Array,
delimiterUrl: String
},
components: { PerfectScrollbar },
data() {
return {
open: false
};
}
};
</script>

View file

@ -1,70 +1,9 @@
<% if @breadcrumbs_items&.length %>
<% shortened = @breadcrumbs_items.length > 4 %>
<% if shortened %>
<% first_breadcrumb_item = @breadcrumbs_items.shift
last_breadcrumb_items = @breadcrumbs_items.pop(2) %>
<%= link_to(first_breadcrumb_item[:label], first_breadcrumb_item[:url],
class: "breadcrumbs-link",
title: first_breadcrumb_item[:label]) %>
<span class="delimiter">
<%= image_tag "icon_small/navigate_next.svg",
alt: "navigate next",
class: "navigate_next" %>
</span>
<span class="breadcrumbs-collapsed-container">
<span class="show-breadcrumbs" data-toggle="dropdown" title="<%= t('projects.index.breadcrumbs_collapsed') %>">
•••
<span class="caret pull-right"></span>
</span>
<ul class="dropdown-menu breadcrumbs-dropdown" role="menu">
<% @breadcrumbs_items.each do |item| %>
<li>
<%= link_to(item[:label], item[:url]) %>
</li>
<% end %>
</ul>
</span>
<span class="delimiter">
<%= image_tag "icon_small/navigate_next.svg",
alt: "navigate next",
class: "navigate_next" %>
</span>
<% item = last_breadcrumb_items.first %>
<%= link_to(item[:label], item[:url],
class: "breadcrumbs-link",
title: item[:label]) %>
<span class="delimiter">
<%= image_tag "icon_small/navigate_next.svg",
alt: "navigate next",
class: "navigate_next" %>
</span>
<% last_item = last_breadcrumb_items.last %>
<span class="breadcrumbs-link" title="<%= last_item[:label] %>">
<%= last_item[:label] %>
</span>
<% else %>
<% last_item = @breadcrumbs_items.pop %>
<% @breadcrumbs_items.each do |item| %>
<% if item[:url] %>
<%= link_to(item[:label], item[:url],
class: "breadcrumbs-link",
title: item[:label]) %>
<% else %>
<%= content_tag(:span, item[:label],
class: "breadcrumbs-link plain-text",
title: item[:label]) %>
<% end %>
<span class="delimiter">
<img src="<%= asset_path "icon_small/navigate_next.svg"%>"
alt="navigate next"
class="navigate_next">
</span>
<% end %>
<span class="breadcrumbs-link" title="<%= last_item[:label] %>">
<%= last_item[:label] %>
</span>
<% end %>
<div id="breadcrumbs" data-behaviour="vue">
<breadcrumbs
breadcrumbs-items="<%= @breadcrumbs_items.to_json %>"
delimiter-url="<%= asset_path "icon_small/navigate_next.svg" %>"
/>
</div>
<%= javascript_include_tag 'vue_navigation_breadcrumbs' %>
<% end %>

View file

@ -33,6 +33,7 @@ const entryList = {
vue_repository_assign_items_to_task_modal: './app/javascript/packs/vue/assign_items_to_task_modal.js',
vue_navigation_top_menu: './app/javascript/packs/vue/navigation/top_menu.js',
vue_navigation_navigator: './app/javascript/packs/vue/navigation/navigator.js',
vue_navigation_breadcrumbs: './app/javascript/packs/vue/navigation/breadcrumbs.js',
vue_components_action_toolbar: './app/javascript/packs/vue/action_toolbar.js'
}