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
26 changes: 10 additions & 16 deletions docs/.docgen/components-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -10679,13 +10679,15 @@
]
},
"CdsSegmentedControl": {
"displayName": "CdsSegmentedControl",
"name": "CdsSegmentedControl",
"exportName": "default",
"displayName": "SegmentedControl",
"description": "",
"tags": {},
"props": [
{
"name": "segments",
"description": "Array de strings que serão exibidos como opções do componente.",
"type": {
"name": "array"
},
Expand All @@ -10696,6 +10698,7 @@
},
{
"name": "withIcon",
"description": "Se verdadeiro, exibe ícones no lugar de texto.",
"type": {
"name": "boolean"
},
Expand All @@ -10706,6 +10709,7 @@
},
{
"name": "segmentsTooltipText",
"description": "Array de strings que serão exibidos como tooltip quando o mouse estiver sobre o ícone.",
"type": {
"name": "array"
},
Expand All @@ -10718,21 +10722,11 @@
"events": [
{
"name": "click",
"type": {
"names": [
"undefined"
]
},
"properties": [
{
"type": {
"names": [
"undefined"
]
},
"name": "<anonymous1>"
}
]
"description": "Evento emitido quando o usuário clica em algum segmento."
},
{
"name": "update:model-value",
"description": "Evento emitido quando o usuário clica em algum segmento."
}
Comment thread
jvictordev1 marked this conversation as resolved.
],
"sourceFiles": [
Expand Down
13 changes: 12 additions & 1 deletion docs/components/navegação/segmented-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,18 @@ SegmentedControls são componentes que permitem que o usuário visualize versõe

## Uso

### Texto
```js
<CdsSegmentedControl
v-model="activeSegment"
:segments="['Segmento 1', 'Segmento 2', 'Segmento 3']"
/>
Comment thread
jvictordev1 marked this conversation as resolved.
```

### Ícone
```js
<CdsSegmentedControl
v-model="activeSegment"
:segments="['info-outline', 'copy-outline', 'edit-outline']"
:segmentsTooltipText="['info', 'copiar', 'editar']"
:withIcon="true"
Expand Down Expand Up @@ -48,7 +58,8 @@ import { ref } from 'vue';
import CdsSegmentedControl from '@/components/SegmentedControl.vue';

const cdsSegmentedControlEvents = [
'click'
'click',
'update:modelValue',
];

const args = ref({
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sysvale/cuida",
"version": "3.158.2",
"version": "3.159.0",
"description": "A design system built by Sysvale, using storybook and Vue components",
"repository": {
"type": "git",
Expand Down
100 changes: 63 additions & 37 deletions src/components/SegmentedControl.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
type="button"
class="segment-control__button"
:class="{
'segment-control__button--active': segment === activeSegment,
'segment-control__button--inactive': segment !== activeSegment,
'segment-control__button--active': segment === model,
'segment-control__button--inactive': segment !== model,
}"
@click="handleClick(segment, index)"
>
Expand All @@ -28,48 +28,74 @@
</button>
</div>
</template>
<script>

<script setup>
import { onMounted } from 'vue';
import CdsIcon from './Icon.vue';
import CdsTooltip from './Tooltip.vue';

export default {
name: 'CdsSegmentedControl',
components: {
CdsIcon,
CdsTooltip,
defineOptions({ name: 'CdsSegmentedControl' });

/**
* Prop utilizada como v-model do componente.
*/
const model = defineModel({
type: String,
default: '',
});

const props = defineProps({
/**
* Array de strings que serão exibidos como opções do componente.
*/
segments: {
type: Array,
default: () => [],
},
props: {
segments: {
type: Array,
default: () => [],
},
withIcon: {
type: Boolean,
default: false,
},
segmentsTooltipText: {
type: Array,
default: () => [],
},
/**
* Se verdadeiro, exibe ícones no lugar de texto.
*/
withIcon: {
type: Boolean,
default: false,
},

data() {
return {
activeSegment: '',
};
/**
* Array de strings que serão exibidos como tooltip quando o mouse estiver sobre o ícone.
*/
segmentsTooltipText: {
type: Array,
default: () => [],
},
});

mounted() {
this.activeSegment = this.segments[0];
},
const emit = defineEmits([
/**
* Evento emitido quando o usuário clica em algum segmento.
* @event click
*/
'click',
/**
* Evento emitido quando o usuário clica em algum segmento.
* @event update:model-value
*/
'update:model-value',
]);

methods: {
handleClick(segment, index) {
this.activeSegment = segment;
this.$emit('click', this.activeSegment, index);
},
},
}
onMounted(() => {
if (!model.value && props.segments.length > 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Valor falsy sobrescrito A condição !model.value trata valores falsy como ausência. Como o componente compara o segmento diretamente com o modelo, um segmento válido como string vazia, 0 ou false pode ser selecionado pelo pai, mas este branch sobrescreve esse valor com o primeiro item no mount. Isso impede o componente de respeitar valores controlados que sejam falsy.

Rule Used: What: Sempre responda em português (PT-BR) durante... (source)

model.value = props.segments[0];
}
Comment on lines +84 to +87

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Atualização no mount Este bloco grava em model.value durante o mount quando o valor inicial é vazio. Em um uso controlado como v-model="selected" com selected = '', o componente emite update:modelValue antes de qualquer clique e troca o estado do pai para o primeiro segmento. Isso pode disparar filtros, watchers ou chamadas de API como se o usuário tivesse feito uma seleção.

Rule Used: What: Sempre responda em português (PT-BR) durante... (source)

});
Comment on lines +84 to +88

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Default não acompanha segmentos A seleção padrão só roda no onMounted. Se o componente montar com segments=[] e a lista chegar depois de uma busca ou renderização condicional, props.segments.length será zero no mount e nenhum watch selecionará o primeiro item quando a lista for preenchida. Nesse fluxo, o componente fica sem segmento ativo mesmo sem valor inicial.

Rule Used: What: Sempre responda em português (PT-BR) durante... (source)


const handleClick = (segment, index) => {
model.value = segment;
/**
* Evento emitido quando o componente é clicado.
* @event click
* @type {Event}
*/
emit('click', segment, index);
};
</script>

<style lang="scss">
Expand Down Expand Up @@ -116,4 +142,4 @@ export default {
}
}
}
</style>
</style>
64 changes: 64 additions & 0 deletions src/tests/SegmentedControlVModel.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, test, expect } from 'vitest';
import SegmentedControl from '../components/SegmentedControl.vue';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';

describe('SegmentedControl v-model', () => {
test('should respect initial modelValue', async () => {
const wrapper = mount(SegmentedControl, {
props: {
segments: ['Segment 1', 'Segment 2'],
modelValue: 'Segment 2',
},
});

const activeButtons = wrapper.findAll('.segment-control__button--active');
expect(activeButtons.length).toBe(1);
expect(activeButtons[0].text()).toBe('Segment 2');
});

test('should update modelValue when a segment is clicked', async () => {
const wrapper = mount(SegmentedControl, {
props: {
segments: ['Segment 1', 'Segment 2'],
modelValue: 'Segment 1',
'onUpdate:modelValue': (e) => wrapper.setProps({ modelValue: e }),
},
});

const buttons = wrapper.findAll('.segment-control__button');
await buttons[1].trigger('click');

expect(wrapper.emitted('update:modelValue')).toBeTruthy();
expect(wrapper.emitted('update:modelValue')![0]).toEqual(['Segment 2']);
});

test('should default to the first segment if no modelValue is provided', async () => {
const wrapper = mount(SegmentedControl, {
props: {
segments: ['Segment 1', 'Segment 2'],
},
});

await nextTick();

const activeButtons = wrapper.findAll('.segment-control__button--active');
expect(activeButtons.length).toBe(1);
expect(activeButtons[0].text()).toBe('Segment 1');
});

test('should still emit click event when a segment is clicked', async () => {
const wrapper = mount(SegmentedControl, {
props: {
segments: ['Segment 1', 'Segment 2'],
modelValue: 'Segment 1',
},
});

const buttons = wrapper.findAll('.segment-control__button');
await buttons[1].trigger('click');

expect(wrapper.emitted('click')).toBeTruthy();
expect(wrapper.emitted('click')![0]).toEqual(['Segment 2', 1]);
});
});
Loading