Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/.docgen/components-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -7325,8 +7325,8 @@
"name": "object"
},
"defaultValue": {
"func": true,
"value": "() => {}"
"func": false,
"value": "{}"
}
},
{
Expand Down
20 changes: 16 additions & 4 deletions docs/components/navegação/mobile-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,22 @@ const items = [
label: 'Vigilância sanitária',
icon: 'shield-outline',
type: 'route',
route: {
path: '/visa',
name: 'visa'
},
items: [
{
label: 'Categorias',
route: {
path: '/categories',
name: 'index-categories',
},
},
{
label: 'Processos',
route: {
path: '/processes',
name: 'index-processes',
},
},
],
},
{
label: 'Central de marcação',
Expand Down
144 changes: 128 additions & 16 deletions src/components/MobileNavigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,73 @@

<div class="mobile-navigation__sidebar-content">
<div class="mobile-navigation__sidebar-items">
<router-link
<div
v-for="item in items"
:key="item.label"
:to="routerPushTo(item)"
class="mobile-navigation__sidebar-item"
:class="{
'mobile-navigation__sidebar-item--active': isActive(item),
}"
@click="handleItemClick(item)"
>
<CdsIcon
:name="item.icon"
width="24"
height="24"
/>
<router-link
v-if="!item.items || item.items.length === 0"
:to="routerPushTo(item)"
class="mobile-navigation__sidebar-item"
:class="{
'mobile-navigation__sidebar-item--active': isActive(item),
}"
@click="handleItemClick(item)"
>
<div class="mobile-navigation__sidebar-item-title">
<CdsIcon
:name="item.icon"
width="24"
height="24"
/>

<span>{{ item.label }}</span>
</div>
</router-link>

<div
v-else
class="mobile-navigation__sidebar-item"
:class="{
'mobile-navigation__sidebar-item--active': isActive(item),
}"
@click="handleItemClick(item)"
>
<div class="mobile-navigation__sidebar-item-title">
<CdsIcon
:name="item.icon"
width="24"
height="24"
/>

<span>{{ item.label }}</span>
</div>

<CdsIcon
:name="expandedItem === item.label ? 'caret-up-outline' : 'caret-down-outline'"
width="16"
height="16"
/>
</div>
Comment on lines +69 to +92
Copy link
Contributor

Choose a reason for hiding this comment

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

cursor: pointer ausente no item pai (elemento div)

Antes desta PR, o item era renderizado como <router-link> (que gera uma tag <a>), a qual possui cursor: pointer por padrão. Agora que itens com subitems são renderizados como <div>, eles herdam o cursor padrão de texto (default), removendo a indicação visual de que o elemento é clicável.

A classe .mobile-navigation__sidebar-item no CSS base não define cursor: pointer explicitamente. O único elemento que define é __sidebar-logout.

Sugestão: adicionar cursor: pointer ao bloco base de __sidebar-item no SCSS:

&__sidebar-item {
    @include tokens.body-1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: tokens.pYX(4, 5);
    border-radius: 10px;
    gap: 10px;
    background: none;
    transition: background 0.25s ease-in-out;
    text-decoration: none;
    cursor: pointer;
    ...
}


<span>{{ item.label }}</span>
</router-link>
<div
v-if="item.items && item.items.length > 0 && expandedItem === item.label"
class="mobile-navigation__subitems"
>
<router-link
v-for="subitem in item.items"
:key="subitem.label"
:to="routerPushTo(subitem)"
class="mobile-navigation__sidebar-subitem"
:class="{
'mobile-navigation__sidebar-subitem--active': isActive(subitem),
}"
@click="handleItemClick(subitem)"
>
<span>{{ subitem.label }}</span>
</router-link>
</div>
</div>
</div>

<div class="mobile-navigation__sidebar-footer">
Expand Down Expand Up @@ -144,7 +193,7 @@ const props = defineProps({
*/
activeItem: {
type: Object,
default: () => {},
default: () => ({}),
},
/**
* Define as informações referentes ao usuário. O objeto deve seguir a assinatura:
Expand Down Expand Up @@ -186,6 +235,7 @@ const emit = defineEmits([

const internalActiveItem = ref(props.activeItem);
const openSidebar = ref(false);
const expandedItem = ref(null);

const resolveMode = computed(() => {
return (props.light) ? 'light' : 'dark';
Expand Down Expand Up @@ -219,9 +269,26 @@ const handleCloseSidebar = () => {
openSidebar.value = false;
};

const isActive = (item) => isEqual(item, internalActiveItem.value);
const isActive = (item) => {
let hasActiveSubitem = false;
let hasActiveItem = false;

if (!!item.items && item.items.length > 0) {
hasActiveSubitem = item.items.some(subitem => {
return isEqual(subitem, internalActiveItem.value);
});
}

hasActiveItem = isEqual(item, internalActiveItem.value);
return hasActiveSubitem || hasActiveItem;
};

const handleItemClick = (item) => {
if (item.items && item.items.length > 0) {
expandedItem.value = expandedItem.value === item.label ? null : item.label;
return;
}

if (isEmpty(props.activeItem)) {
internalActiveItem.value = item;
}
Expand Down Expand Up @@ -327,11 +394,36 @@ const mustDisableExternalScrolls = (value) => {
@include tokens.body-1;
display: flex;
align-items: center;
justify-content: space-between;
padding: tokens.pYX(4, 5);
border-radius: 10px;
gap: 10px;
background: none;
transition: background 0.25s ease-in-out;
text-decoration: none;

&--active {
font-weight: 700;
}
}

&__sidebar-item-title {
display: flex;
align-items: center;
gap: 10px;
}

&__subitems {
display: flex;
flex-direction: column;
gap: tokens.spacer(2);
padding: tokens.pTRBL(2, 0, 4, 10);
}

&__sidebar-subitem {
@include tokens.body-2;
padding: tokens.py(2);
text-decoration: none;

&--active {
font-weight: 700;
Expand Down Expand Up @@ -390,6 +482,8 @@ const mustDisableExternalScrolls = (value) => {
}

&__sidebar-item {
color: tokens.$n-0;

&--active {
background: #576169;
color: tokens.$n-0;
Expand All @@ -398,6 +492,14 @@ const mustDisableExternalScrolls = (value) => {
}
}

&__sidebar-subitem {
color: tokens.$n-100;

&--active {
color: tokens.$n-0;
}
}

&__sidebar-logout {
color: tokens.$n-0;
}
Expand Down Expand Up @@ -425,13 +527,23 @@ const mustDisableExternalScrolls = (value) => {
}

&__sidebar-item {
color: tokens.$n-700;

&--active {
background: var(--system-background-variant);
color: var(--system-text-variant);
border: 1px solid var(--system-border-variant);
}
}

&__sidebar-subitem {
color: tokens.$n-600;

&--active {
color: var(--system-text-variant);
}
}

&__sidebar-logout {
color: tokens.$n-700;
}
Expand Down
91 changes: 91 additions & 0 deletions src/tests/MobileNavigationSubitems.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, test, expect } from 'vitest';
import MobileNavigation from '../components/MobileNavigation.vue';
import { mount } from '@vue/test-utils';

const mockedData = [
{
label: 'Início',
icon: 'home-outline',
route: {
path: '/home',
name: 'home'
},
},
{
label: 'Agendamentos',
icon: 'calendar-outline',
items: [
{
label: 'Categorias',
route: {
path: '/categories',
name: 'index-categories',
},
},
],
},
];

describe('MobileNavigation subitems', () => {
test('renders items with subitems correctly', async () => {
const wrapper = mount(MobileNavigation, {
global: {
stubs: {
'cds-icon': true,
'cds-avatar': true,
'router-link': { template: '<a><slot /></a>' },
},
},
props: {
items: mockedData,
user: {
name: 'Joana Mendes',
role: 'Administradora',
}
},
});

// Check if the parent item is rendered
expect(wrapper.text()).toContain('Agendamentos');

// Check if subitems are initially not rendered
expect(wrapper.text()).not.toContain('Categorias');

// Find the parent item with subitems and click it to expand
const parentItem = wrapper.findAll('.mobile-navigation__sidebar-item').filter(w => w.text().includes('Agendamentos'))[0];
await parentItem.trigger('click');

// Check if subitems are now rendered
expect(wrapper.text()).toContain('Categorias');

// Check if isActive correctly identifies active parent when subitem is active
const subitem = wrapper.find('.mobile-navigation__sidebar-subitem');
// We can't easily test router-link active state here without a real router,
// but we can test the isActive logic if we mount with an activeItem that is a subitem.
});

test('identifies active parent when subitem is active', async () => {
const subitem = mockedData[1].items[0];
const wrapper = mount(MobileNavigation, {
global: {
stubs: {
'cds-icon': true,
'cds-avatar': true,
'router-link': { template: '<a><slot /></a>' },
},
},
props: {
items: mockedData,
activeItem: subitem,
user: {
name: 'Joana Mendes',
role: 'Administradora',
}
},
});

// The parent item "Agendamentos" should have the active class
const parentItem = wrapper.findAll('.mobile-navigation__sidebar-item').filter(w => w.text().includes('Agendamentos'))[0];
expect(parentItem.classes()).toContain('mobile-navigation__sidebar-item--active');
});
});
15 changes: 12 additions & 3 deletions src/tests/__snapshots__/MobileNavigation.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,18 @@ exports[`MobileNavigation > renders correctly 1`] = `
</div>
<div data-v-c1f5c1c8="" class="mobile-navigation__sidebar-content">
<div data-v-c1f5c1c8="" class="mobile-navigation__sidebar-items">
<router-link-stub data-v-c1f5c1c8="" to="/home" class="mobile-navigation__sidebar-item"></router-link-stub>
<router-link-stub data-v-c1f5c1c8="" to="/visa" class="mobile-navigation__sidebar-item mobile-navigation__sidebar-item--active"></router-link-stub>
<router-link-stub data-v-c1f5c1c8="" to="/regulation" class="mobile-navigation__sidebar-item"></router-link-stub>
<div data-v-c1f5c1c8="">
<router-link-stub data-v-c1f5c1c8="" to="/home" class="mobile-navigation__sidebar-item"></router-link-stub>
<!--v-if-->
</div>
<div data-v-c1f5c1c8="">
<router-link-stub data-v-c1f5c1c8="" to="/visa" class="mobile-navigation__sidebar-item mobile-navigation__sidebar-item--active"></router-link-stub>
<!--v-if-->
</div>
<div data-v-c1f5c1c8="">
<router-link-stub data-v-c1f5c1c8="" to="/regulation" class="mobile-navigation__sidebar-item"></router-link-stub>
<!--v-if-->
</div>
</div>
<div data-v-c1f5c1c8="" class="mobile-navigation__sidebar-footer">
<div data-v-c1f5c1c8="" class="mobile-navigation__sidebar-user-info">
Expand Down
Loading