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
22 changes: 6 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,7 @@
"events": [
{
"name": "click",
"type": {
"names": [
"undefined"
]
},
"properties": [
{
"type": {
"names": [
"undefined"
]
},
"name": "<anonymous1>"
}
]
"description": "Evento emitido quando o componente é clicado."
}
Comment on lines 10722 to 10726
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.

P2 Evento ausente na API O metadata do CdsSegmentedControl continua listando apenas o evento click. Como o PR adiciona v-model, a tabela de eventos renderizada por APITable não mostra update:modelValue, embora a página de docs já tenha incluído esse evento no preview. O novo contrato fica documentado de forma inconsistente.

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

],
"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 on lines 11 to +14
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.

P2 Exemplo sem estado O exemplo novo usa v-model="activeSegment", mas a página não declara activeSegment no <script setup>. Quem copiar o exemplo com o script da página ficará com um binding inexistente, e se o trecho for renderizado em Vue ele acessará uma variável não definida. Declare o ref usado no exemplo ou ajuste o snippet para usar um estado existente.

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

```

### Í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
95 changes: 58 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,69 @@
</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 componente é clicado.
* @event click
*/
'click',
]);

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 +79 to +82
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 +79 to +83
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 +137,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