diff --git a/docs/userGuide/syntax/cardstacks.md b/docs/userGuide/syntax/cardstacks.md index a04122e701..e392fa304c 100644 --- a/docs/userGuide/syntax/cardstacks.md +++ b/docs/userGuide/syntax/cardstacks.md @@ -200,15 +200,42 @@ The `` element allows you to: - Bootstrap color names (e.g., `success`, `danger`, `primary`, `warning`, `info`, `secondary`, `light`, `dark`) - Any tags used in cards but not defined in `` will appear after the defined tags with default colors +### Disabling Tag Counts + +By default, tag badges display a count showing how many cards have that tag. You can disable this count display using the `disable-tag-count` attribute: + + +html + + + + Success is not final, failure is not fatal: it is the courage to continue that counts + + + In the middle of every difficulty lies opportunity + + + Do what you can, with what you have, where you are + + + Your time is limited, so don't waste it living someone else's life + + + + + +With `disable-tag-count` enabled, tag badges will only show the tag name and selection indicator, without the numerical count. + **Options** `cardstack`: -| Name | Type | Default | Description | -| --------------- | --------- | ------- | --------------------------------------------------------------------------------- | -| blocks | `String` | `2` | Number of `card` columns per row.
Supports: `1`, `2`, `3`, `4`, `6` | -| searchable | `Boolean` | `false` | Whether the card stack is searchable. | -| show-select-all | `Boolean` | `true` | Whether the select all tag button appears. (`false` by default if total tags ≤ 3) | +| Name | Type | Default | Description | +| ------------------ | --------- | ------- | --------------------------------------------------------------------------------- | +| blocks | `String` | `2` | Number of `card` columns per row.
Supports: `1`, `2`, `3`, `4`, `6` | +| searchable | `Boolean` | `false` | Whether the card stack is searchable. | +| show-select-all | `Boolean` | `true` | Whether the select all tag button appears. (`false` by default if total tags ≤ 3) | +| disable-tag-count | `Boolean` | `false` | Whether to hide the tag count badges. By default, counts are shown. | `tags` (optional): A container element inside `cardstack` to define tag ordering and colors. diff --git a/packages/vue-components/src/__tests__/CardStack.spec.js b/packages/vue-components/src/__tests__/CardStack.spec.js index 9c6c7d7e44..7f8471ce28 100644 --- a/packages/vue-components/src/__tests__/CardStack.spec.js +++ b/packages/vue-components/src/__tests__/CardStack.spec.js @@ -317,7 +317,7 @@ describe('CardStack', () => { }); test('should handle invalid tag-configs gracefully', async () => { - const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => { }); const wrapper = mount(CardStack, { propsData: { dataTagConfigs: 'invalid-json', @@ -366,4 +366,166 @@ describe('CardStack', () => { expect(wrapper.vm.getTextColor('#000000')).toBe('#fff'); expect(wrapper.vm.getTextColor('#333333')).toBe('#fff'); }); + + test('should initialize tag count correctly for custom tag configs', async () => { + const tagConfigs = JSON.stringify([ + { name: 'Success', color: '#28a745' }, + { name: 'Failure', color: '#dc3545' }, + ]); + const wrapper = mount(CardStack, { + propsData: { + dataTagConfigs: tagConfigs.replace(/"/g, '"'), + }, + slots: { default: CARDS_WITH_CUSTOM_TAGS }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + const { tagMapping } = wrapper.vm.cardStackRef; + // Custom tags in config that appear in cards should be incremented (Success and Failure appear once each) + expect(tagMapping[0][1].count).toBe(1); + expect(tagMapping[1][1].count).toBe(1); + // Remaining tags not in config should have count 1 + expect(tagMapping[2][1].count).toBe(1); + }); + + test('should increment tag count when same tag appears in multiple cards', async () => { + const CARDS_WITH_DUPLICATE_TAGS = ` + + + + + + `; + const wrapper = mount(CardStack, { + slots: { default: CARDS_WITH_DUPLICATE_TAGS }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + const { tagMapping } = wrapper.vm.cardStackRef; + // Tag1 appears 3 times, Tag2 appears 2 times + expect(tagMapping[0][0]).toBe('Tag1'); + expect(tagMapping[0][1].count).toBe(3); + expect(tagMapping[1][0]).toBe('Tag2'); + expect(tagMapping[1][1].count).toBe(2); + }); + + test('should display tag count in the badge', async () => { + const CARDS_WITH_DUPLICATE_TAGS = ` + + + + `; + const wrapper = mount(CardStack, { + slots: { default: CARDS_WITH_DUPLICATE_TAGS }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + // Check that count badge exists and displays correct numbers + const tagBadges = wrapper.findAll('.tag-badge'); + // First tag (Success) should show count 2 + expect(tagBadges[0].text()).toContain('Success'); + const firstTagCountBadge = tagBadges[0].find('.tag-count'); + expect(firstTagCountBadge.text()).toBe('2'); + + // Second tag (Failure) should show count 1 + expect(tagBadges[1].text()).toContain('Failure'); + const secondTagCountBadge = tagBadges[1].findAll('.tag-count')[0]; + expect(secondTagCountBadge.text()).toBe('1'); + }); + + test('should show count badge before the select indicator badge', async () => { + const CARDS_WITH_DUPLICATE_TAGS = ` + + + `; + const wrapper = mount(CardStack, { + slots: { default: CARDS_WITH_DUPLICATE_TAGS }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + const firstTagBadge = wrapper.find('.tag-badge'); + const tagIndicators = firstTagBadge.findAll('.badge'); + // Should have two indicators: count badge and select badge + expect(tagIndicators.length).toBe(2); + // First one is count, should display "2" + expect(tagIndicators[0].text()).toBe('2'); + // Second one is select indicator, should display ✓ (since allSelected is true initially) + expect(tagIndicators[1].text()).toContain('✓'); + }); + + test('should hide tag count when disableTagCount is true', async () => { + const CARDS_WITH_DUPLICATE_TAGS = ` + + + + `; + const wrapper = mount(CardStack, { + propsData: { + disableTagCount: true, + }, + slots: { default: CARDS_WITH_DUPLICATE_TAGS }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + const firstTagBadge = wrapper.find('.tag-badge'); + const countBadge = firstTagBadge.find('.tag-count'); + // Count badge should not exist when disableTagCount is true + expect(countBadge.exists()).toBe(false); + + // Should only have select indicator badge + const tagIndicators = firstTagBadge.findAll('.tag-indicator'); + expect(tagIndicators.length).toBe(1); + // The only indicator should be the select indicator with ✓ + expect(tagIndicators[0].text()).toContain('✓'); + }); + + test('should show tag count by default when disableTagCount is false', async () => { + const CARDS_WITH_DUPLICATE_TAGS = ` + + + + `; + const wrapper = mount(CardStack, { + propsData: { + disableTagCount: false, + }, + slots: { default: CARDS_WITH_DUPLICATE_TAGS }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + const firstTagBadge = wrapper.find('.tag-badge'); + const countBadge = firstTagBadge.find('.tag-count'); + // Count badge should exist when disableTagCount is false + expect(countBadge.exists()).toBe(true); + expect(countBadge.text()).toBe('2'); + + // Should have both count and select indicator badges + const tagIndicators = firstTagBadge.findAll('.badge'); + expect(tagIndicators.length).toBe(2); + }); + + test('should show tag count by default when disableTagCount is not specified', async () => { + const CARDS_WITH_DUPLICATE_TAGS = ` + + + + `; + const wrapper = mount(CardStack, { + slots: { default: CARDS_WITH_DUPLICATE_TAGS }, + global: DEFAULT_GLOBAL_MOUNT_OPTIONS, + }); + await wrapper.vm.$nextTick(); + + const firstTagBadge = wrapper.find('.tag-badge'); + const countBadge = firstTagBadge.find('.tag-count'); + // Count badge should exist by default (disableTagCount defaults to false) + expect(countBadge.exists()).toBe(true); + expect(countBadge.text()).toBe('3'); + }); }); diff --git a/packages/vue-components/src/__tests__/__snapshots__/CardStack.spec.js.snap b/packages/vue-components/src/__tests__/__snapshots__/CardStack.spec.js.snap index d3b3e71494..cc25da812a 100644 --- a/packages/vue-components/src/__tests__/__snapshots__/CardStack.spec.js.snap +++ b/packages/vue-components/src/__tests__/__snapshots__/CardStack.spec.js.snap @@ -108,6 +108,11 @@ exports[`CardStack should not hide cards when no filter is provided 1`] = ` class="badge bg-primary tag-badge" > Short  + + 1 + diff --git a/packages/vue-components/src/cardstack/CardStack.vue b/packages/vue-components/src/cardstack/CardStack.vue index 3f9d32e4e4..f0c623cc27 100644 --- a/packages/vue-components/src/cardstack/CardStack.vue +++ b/packages/vue-components/src/cardstack/CardStack.vue @@ -34,6 +34,9 @@ @click="updateTag(key[0])" > {{ key[0] }}  + + {{ key[1].count }} +     @@ -71,6 +74,10 @@ export default { type: Boolean, default: false, }, + disableTagCount: { + type: Boolean, + default: false, + }, tagConfigs: { type: String, default: '', @@ -189,7 +196,9 @@ export default { customConfigs.forEach((config) => { if (tags.includes(config.name)) { const color = normalizeColor(config.color) || BADGE_COLOURS[index % BADGE_COLOURS.length]; - const tagMapping = { badgeColor: color, children: [], disableTag: false }; + const tagMapping = { + badgeColor: color, children: [], disableTag: false, count: 0, + }; tagMap.set(config.name, tagMapping); index += 1; } @@ -199,9 +208,13 @@ export default { tags.forEach((tag) => { if (!tagMap.has(tag)) { const color = BADGE_COLOURS[index % BADGE_COLOURS.length]; - const tagMapping = { badgeColor: color, children: [], disableTag: false }; + const tagMapping = { + badgeColor: color, children: [], disableTag: false, count: 1, + }; tagMap.set(tag, tagMapping); index += 1; + } else { + tagMap.get(tag).count += 1; } }); @@ -285,13 +298,20 @@ export default { } .tag-badge { - margin: 2px; cursor: pointer; height: inherit; padding: 5px; } + .tag-count { + margin: 2px; + + /* set radius to a huge value to ensure always rounded corners */ + border-radius: 999px; + } + .tag-indicator { + margin: 1px; width: 18px; height: 100%; }