Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Filter counter in button and tab #2143

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
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
53 changes: 20 additions & 33 deletions src/components/VHeader/VFilterButton.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
<template>
<VButton
id="filter-button"
:variant="filtersAreApplied ? 'action-menu-muted' : 'action-menu'"
variant="plain"
size="disabled"
class="align-center label-regular h-12 w-12 gap-2 self-center xl:w-auto xl:ps-3"
:class="[filtersAreApplied ? 'xl:pe-3' : 'xl:pe-4']"
class="align-center label-regular h-12 w-12 gap-2 self-center border-tx xl:w-auto xl:ps-3 xl:pe-4"
:class="
pressed
? 'bg-dark-charcoal text-white hover:bg-dark-charcoal-90'
: 'bg-tx hover:border-dark-charcoal-20'
"
:pressed="pressed"
:disabled="disabled"
aria-controls="filters"
:aria-label="xlMinLabel"
:aria-label="ariaLabel"
@click="$emit('toggle')"
@keydown.tab.exact="$emit('tab', $event)"
>
<VIcon
:class="filtersAreApplied ? 'hidden' : 'block'"
:icon-path="filterIcon"
<VFilterIconOrCounter
:applied-filter-count="filterCount"
:pressed="pressed"
/>
<span class="hidden xl:inline-block">{{ xlMinLabel }}</span>
<span class="xl:hidden" :class="{ hidden: !filtersAreApplied }">{{
lgMaxLabel
}}</span>
<span class="hidden xl:inline-block">{{ textLabel }}</span>
</VButton>
</template>

Expand All @@ -31,14 +31,12 @@ import { defineEvent } from "~/types/emits"
import { useI18n } from "~/composables/use-i18n"

import VButton from "~/components/VButton.vue"
import VIcon from "~/components/VIcon/VIcon.vue"

import filterIcon from "~/assets/icons/filter.svg"
import VFilterIconOrCounter from "~/components/VHeader/VFilterIconOrCounter.vue"

export default defineComponent({
name: "VFilterButton",
components: {
VIcon,
VFilterIconOrCounter,
VButton,
},
props: {
Expand All @@ -52,7 +50,6 @@ export default defineComponent({
},
},
emits: {
tab: defineEvent<[KeyboardEvent]>(),
toggle: defineEvent(),
},
setup() {
Expand All @@ -61,26 +58,16 @@ export default defineComponent({
const filterCount = computed(() => searchStore.appliedFilterCount)
const filtersAreApplied = computed(() => filterCount.value > 0)

/**
* This label's verbosity makes it useful for the aria-label
* where it is also used, especially on mobile where the
* label would just be the number of applied filters, and therefore
* basically useless as far as a label is concerned!
*/
const xlMinLabel = computed(() =>
filtersAreApplied.value
? i18n.tc("header.filter-button.with-count", filterCount.value)
: i18n.t("header.filter-button.simple")
)
const lgMaxLabel = computed(() =>
filtersAreApplied ? filterCount.value : ""
const textLabel = computed(() => i18n.t("header.filter-button.simple"))
const ariaLabel = computed(() =>
i18n.tc("header.filter-button.with-count", filterCount.value)
)

return {
filterIcon,
xlMinLabel,
lgMaxLabel,
ariaLabel,
textLabel,
filtersAreApplied,
filterCount,
}
},
})
Expand Down
40 changes: 40 additions & 0 deletions src/components/VHeader/VFilterIconOrCounter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<template>
<VIcon v-if="showIcon" :icon-path="filterIcon" />
<p
v-else
class="flex h-6 w-6 items-center justify-center"
:class="pressed ? 'bg-tx' : 'bg-dark-charcoal-10'"
>
{{ appliedFilterCount }}
</p>
</template>
<script lang="ts">
import { computed, defineComponent } from "@nuxtjs/composition-api"

import VIcon from "~/components/VIcon/VIcon.vue"

import filterIcon from "~/assets/icons/filter.svg"

export default defineComponent({
name: "VFilterIconOrCounter",
components: { VIcon },
props: {
appliedFilterCount: {
type: Number,
default: 0,
},
pressed: {
type: Boolean,
default: false,
},
},
setup(props) {
const showIcon = computed(() => props.appliedFilterCount === 0)

return {
showIcon,
filterIcon,
}
},
})
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
size="medium"
class="label-regular gap-x-2"
>
<VIcon :icon-path="filtersIcon" />{{ $t("filters.title") }}</VTab
<VFilterIconOrCounter :applied-filter-count="appliedFilterCount" />{{
$t("filters.title")
}}</VTab
>
<VIconButton
class="self-center ms-auto hover:bg-dark-charcoal hover:text-white"
Expand Down Expand Up @@ -85,6 +87,7 @@ import { useI18n } from "~/composables/use-i18n"
import useSearchType from "~/composables/use-search-type"

import VButton from "~/components/VButton.vue"
import VFilterIconOrCounter from "~/components/VHeader/VFilterIconOrCounter.vue"
import VIcon from "~/components/VIcon/VIcon.vue"
import VIconButton from "~/components/VIconButton/VIconButton.vue"
import VModalContent from "~/components/VModal/VModalContent.vue"
Expand All @@ -101,6 +104,7 @@ import filtersIcon from "~/assets/icons/filter.svg"
export default defineComponent({
name: "VContentSettingsModalContent",
components: {
VFilterIconOrCounter,
VIcon,
VModalContent,
VButton,
Expand Down
13 changes: 7 additions & 6 deletions src/components/VHeader/meta/VFilterButton.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,19 @@ import { IMAGE } from "~/constants/media"
appliedFilters: {
type: "number",
},
disabled: {
type: "boolean",
},
toggle: {
action: "toggle",
},
tab: {
action: "tab",
},
}}
/>

export const Template = (args, { argTypes }) => ({
template: `<VFilterButton v-bind="args" v-on="args" />`,
template: `<div id="wrapper" class="w-40 h-16 bg-dark-charcoal-06 flex align-center justify-center">
<VFilterButton v-bind="args" v-on="args" />
</div>`,
components: { VFilterButton },
props: Object.keys(argTypes),
setup() {
Expand Down Expand Up @@ -67,8 +69,7 @@ export const Template = (args, { argTypes }) => ({
<ArgsTable of={VFilterButton} />

The button opens and closes the filters sidebar. It also shows how many filters
are applied in the mobile view. the field receives an input. It also emits the
`search` event when the search button is clicked.
are applied. It also emits the `toggle` event when clicked.

<Canvas>
<Story
Expand Down
2 changes: 1 addition & 1 deletion test/playwright/e2e/filters.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ breakpoints.describeMobileAndDesktop(() => {
const filterButtonText = await page
.locator('[aria-controls="filters"] span:visible')
.textContent()
expect(filterButtonText).toContain("1")
expect(filterButtonText).toContain("Filters")
} else {
const filtersAriaLabel =
(await page
Expand Down
Binary file not shown.
32 changes: 25 additions & 7 deletions test/playwright/visual-regression/components/filters.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { expect, test } from "@playwright/test"
import { expect, Page, test } from "@playwright/test"

import {
goToSearchTerm,
languageDirections,
t,
pathWithDir,
} from "~~/test/playwright/utils/navigation"
import breakpoints from "~~/test/playwright/utils/breakpoints"

test.describe.configure({ mode: "parallel" })

const filtersCheckedUrl =
"search/image?q=birds&category=photograph,illustration,digitized_artwork&extension=jpg,png,gif,svg&aspect_ratio=tall,wide,square&size=small,medium"

const openFiltersTab = async (page: Page) => {
await page.locator("#content-settings-button").click()
await page.getByRole("tab").last().click()
}

for (const dir of languageDirections) {
breakpoints.describeEachDesktop(() => {
test(`Filters sidebar none selected - ${dir}`, async ({ page }) => {
Expand All @@ -19,7 +27,7 @@ for (const dir of languageDirections) {
)
})

test(`Filters sidebar some filters selected - ${dir}`, async ({ page }) => {
test(`Filters sidebar 1 filter selected - ${dir}`, async ({ page }) => {
await goToSearchTerm(page, "birds", { dir })
await page.locator('input[type="checkbox"]').first().check()

Expand All @@ -32,20 +40,30 @@ for (const dir of languageDirections) {
breakpoints.describeEachMobile(({ expectSnapshot }) => {
test(`Filters modal none selected - ${dir}`, async ({ page }) => {
await goToSearchTerm(page, "birds", { dir })
await page.getByRole("button", { name: "Menu" }).click()
await page.getByRole("tab", { name: t("filters.title", dir) }).click()
await openFiltersTab(page)

await expectSnapshot(`filters-modal-${dir}.png`, page)
})

test(`Filters modal some filters selected - ${dir}`, async ({ page }) => {
await goToSearchTerm(page, "birds", { dir })
await page.getByRole("button", { name: "Menu" }).click()
await page.getByRole("tab", { name: t("filters.title", dir) }).click()
await openFiltersTab(page)

await page.locator('input[type="checkbox"]').first().check()

await expectSnapshot(`filters-modal-filters-selected-${dir}.png`, page)
})

test(`Filters modal with two-digits count label - ${dir}`, async ({
page,
}) => {
await page.goto(pathWithDir(filtersCheckedUrl, dir))
await openFiltersTab(page)

await expectSnapshot(
`filters-modal-two-digits-count-checked-${dir}.png`,
page
)
})
})
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 60 additions & 35 deletions test/storybook/visual-regression/v-filter-button.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,67 @@ const gotoWithArgs = makeGotoWithArgs(

test.describe.configure({ mode: "parallel" })

test.describe("VFilterButton", () => {
breakpoints.describeLg(({ expectSnapshot }) => {
test("no filters applied", async ({ page }) => {
await gotoWithArgs(page, { isMinMd: true })
await expectSnapshot("filter-button-at-rest", page)
})

test("no filters pressed", async ({ page }) => {
await gotoWithArgs(page, { pressed: true })
await expectSnapshot("filter-button-pressed", page)
})

test("filters applied", async ({ page }) => {
await gotoWithArgs(page, { appliedFilters: 2 })
await expectSnapshot("filter-button-2-filters", page)
})
const wrapper = "#wrapper"

test("filters applied and pressed", async ({ page }) => {
await gotoWithArgs(page, {
isMinMd: true,
appliedFilters: 2,
pressed: true,
test.describe("VFilterButton", () => {
breakpoints.describeMobileAndDesktop(({ expectSnapshot }) => {
for (const filterCount of [0, 1, 12]) {
test(`resting, ${filterCount} filters`, async ({ page }) => {
await gotoWithArgs(page, { appliedFilters: filterCount })
await expectSnapshot(
`filter-button-at-rest-${filterCount}-checked`,
page.locator(wrapper)
)
})
await expectSnapshot("filter-button-2-filters-pressed", page)
})
})

breakpoints.describeXl(({ expectSnapshot }) => {
test("no filters applied", async ({ page }) => {
await gotoWithArgs(page)
await expectSnapshot("filter-button-no-filters-not-scrolled", page)
})

test("2 filters", async ({ page }) => {
await gotoWithArgs(page, { appliedFilters: 2 })
await expectSnapshot("filter-button-2-filters-not-scrolled", page)
})
test(`focused, ${filterCount} filters`, async ({ page }) => {
await gotoWithArgs(page, { appliedFilters: filterCount })
await expectSnapshot(
`filter-button-at-rest-${filterCount}-checked`,
page.locator(wrapper)
)
})
test(`pressed, ${filterCount} filters`, async ({ page }) => {
await gotoWithArgs(page, {
appliedFilters: filterCount,
pressed: true,
})
await expectSnapshot(
`filter-button-pressed-${filterCount}-checked`,
page.locator(wrapper)
)
})
test(`pressed, hovered, ${filterCount} filters`, async ({ page }) => {
await gotoWithArgs(page, {
appliedFilters: filterCount,
pressed: true,
})
await page.locator("button", { hasText: "Filter" }).hover()
await expectSnapshot(
`filter-button-pressed-hovered-${filterCount}-checked`,
page.locator(wrapper)
)
})
test(`hovered, ${filterCount} filters`, async ({ page }) => {
await gotoWithArgs(page, {
appliedFilters: filterCount,
})
await page.locator("button", { hasText: "Filter" }).hover()
await expectSnapshot(
`filter-button-hovered-${filterCount}-checked`,
page.locator(wrapper)
)
})
test(`focused, pressed ${filterCount} filters`, async ({ page }) => {
await gotoWithArgs(page, {
appliedFilters: filterCount,
pressed: true,
})
await page.locator("button", { hasText: "Filter" }).focus()
await expectSnapshot(
`filter-button-focused-pressed-${filterCount}-checked`,
page.locator(wrapper)
)
})
}
})
})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading