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
37 changes: 32 additions & 5 deletions docs/userGuide/syntax/cardstacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,15 +200,42 @@ The `<tags>` 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 `<tags>` 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:

<include src="codeAndOutput.md" boilerplate >
<variable name="highlightStyle">html</variable>
<variable name="code">
<cardstack searchable disable-tag-count>
<card header="**Winston Churchill**" tag="Success, Perseverance">
Success is not final, failure is not fatal: it is the courage to continue that counts
</card>
<card header="**Albert Einstein**" tag="Success, Perseverance">
In the middle of every difficulty lies opportunity
</card>
<card header="**Theodore Roosevelt**" tag="Motivation, Hard Work">
Do what you can, with what you have, where you are
</card>
<card header="**Steve Jobs**" tag="Happiness, Mindset">
Your time is limited, so don't waste it living someone else's life
</card>
</cardstack>
</variable>
</include>

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.<br> 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.<br> 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.
Expand Down
164 changes: 163 additions & 1 deletion packages/vue-components/src/__tests__/CardStack.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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, '&quot;'),
},
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 = `
<card header="Card 1" tag="Tag1"></card>
<card header="Card 2" tag="Tag1"></card>
<card header="Card 3" tag="Tag1"></card>
<card header="Card 4" tag="Tag2"></card>
<card header="Card 5" tag="Tag2"></card>
`;
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 = `
<card header="Card 1" tag="Success"></card>
<card header="Card 2" tag="Success"></card>
<card header="Card 3" tag="Failure"></card>
`;
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 = `
<card header="Card 1" tag="Tag1"></card>
<card header="Card 2" tag="Tag1"></card>
`;
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 = `
<card header="Card 1" tag="Tag1"></card>
<card header="Card 2" tag="Tag1"></card>
<card header="Card 3" tag="Tag2"></card>
`;
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 = `
<card header="Card 1" tag="Tag1"></card>
<card header="Card 2" tag="Tag1"></card>
<card header="Card 3" tag="Tag2"></card>
`;
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 = `
<card header="Card 1" tag="Success"></card>
<card header="Card 2" tag="Success"></card>
<card header="Card 3" tag="Success"></card>
`;
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');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ exports[`CardStack should not hide cards when no filter is provided 1`] = `
class="badge bg-primary tag-badge"
>
Short 
<span
class="badge tag-count bg-light text-dark"
>
1
</span>
<span
class="badge bg-light text-dark tag-indicator"
>
Expand Down
26 changes: 23 additions & 3 deletions packages/vue-components/src/cardstack/CardStack.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
@click="updateTag(key[0])"
>
{{ key[0] }}&nbsp;
<span v-if="!disableTagCount" class="badge tag-count bg-light text-dark">
{{ key[1].count }}
</span>
<span class="badge bg-light text-dark tag-indicator">
<span v-if="computeShowTag(key[0])">✓</span>
<span v-else>&nbsp;&nbsp;&nbsp;</span>
Expand Down Expand Up @@ -71,6 +74,10 @@ export default {
type: Boolean,
default: false,
},
disableTagCount: {
type: Boolean,
default: false,
},
tagConfigs: {
type: String,
default: '',
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
});

Expand Down Expand Up @@ -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%;
}
Expand Down