Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Apps-3035 create filters drop down component #664

Merged
merged 11 commits into from
Dec 13, 2024
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"sass": "^1.79.5",
"storybook": "^7.4.1",
"typescript": "^5.6.3",
"ucla-library-design-tokens": "^5.27.1",
"ucla-library-design-tokens": "^5.28.0",
"video.js": "^8.5.2",
"vite": "^5.4.8",
"vite-svg-loader": "^5.1.0",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

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

114 changes: 114 additions & 0 deletions src/lib-components/FiltersDropdown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { PropType } from 'vue'
import SvgGlyphX from 'ucla-library-design-tokens/assets/svgs/icon-ftva-xtag.svg'
import SvgFilterIcon from 'ucla-library-design-tokens/assets/svgs/icon-ftva-filter.svg'
import BlockTag from './BlockTag.vue'
import ButtonLink from './ButtonLink.vue'
import MobileDrawer from './MobileDrawer.vue'
import { useTheme } from '@/composables/useTheme'

// PROPS
interface FilterGroupsTypes {
name: string
searchField: string
options: string[]
}
const { filterGroups } = defineProps({
filterGroups: {
type: Array as PropType<FilterGroupsTypes[]>,
default: () => [],
}
})
const emit = defineEmits(['update-display'])
// V-MODEL DATA
interface SelectedFiltersTypes {
[key: string]: string[]
}
const selectedFilters = defineModel('selectedFilters', { type: Object as PropType<SelectedFiltersTypes>, required: true, default: {} })
// FUNCTIONS
// calc # for UI '# selected' display
const numOfSelectedFilters = computed(() => {
let count = 0
// for each key in selectedFilters
for (const key in selectedFilters.value as any) {
// add the length of the array of selectedFilters[key]
count += selectedFilters.value[key].length
}
return count
})
// check if option is selected so we can display 'x' SVG
function isSelected(searchField: string, option: string) {
// check if selectedFilter object has any keys, fail gracefully if it doesn't
if (!Object.keys(selectedFilters.value).length)
return null

return selectedFilters.value[searchField].includes(option)
}
// Clear Button Click / clear all selected filters
function clearFilters() {
for (const group of filterGroups)
selectedFilters.value[group.searchField] = []
}
// Done Button Click / emit selected filters to parent
function onDoneClick() {
emit('update-display', selectedFilters.value)
}

// THEME
const theme = useTheme()
const parsedClasses = computed(() => {
return ['filters-dropdown', theme?.value || '']
})
</script>

<template>
<div :class="parsedClasses">
<MobileDrawer>
<template #buttonLabel>
<div class="filter-summary">
Filters ({{ numOfSelectedFilters }} selected )
</div>
<span class="icon-svg">
<SvgFilterIcon aria-hidden="true" />
</span>
</template>
<template #dropdownItems="{ removeOverlay }">
<div class="dropdown-filter">
<div v-for="group in filterGroups" :key="group.name" class="filter-group">
<h3>{{ group.name }}</h3>
<div class="pills">
<!-- <label> must wrap <input> for accessbility fuctionality -->
<label v-for="option in group.options" :key="option" class="pill-label">
<!-- Hidden checkbox for managing selection & screen-reader user interaction -->
<input
:id="option" v-model="selectedFilters[group.searchField]" type="checkbox" class="pill-checkbox" :name="option"
:value="option"
>
<!-- BlockTag component for display -->
<BlockTag :label="option" :is-secondary="true">
<!-- 'x' SVG only shows when selected -->
<template v-if="isSelected(group.searchField, option)">
<SvgGlyphX class="close-icon" />
</template>
</BlockTag>
</label>
</div>
</div>
<div class="action-row">
<ButtonLink class="action-row-button select-button" label="Done" icon-name="none" @click="onDoneClick(); removeOverlay();" />
<ButtonLink
class="action-row-button clear-button" label="Clear" icon-name="icon-close"
@click="clearFilters"
/>
</div>
</div>
</template>
</MobileDrawer>
</div>
</template>

<style lang="scss" scoped>
@import "@/styles/default/_filters-dropdown.scss";
@import "@/styles/ftva/_filters-dropdown.scss";
</style>
2 changes: 1 addition & 1 deletion src/lib-components/MobileDrawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ onMounted(() => {

<template>
<div :class="parsedClasses">
<div v-on-click-outside="closeDropdownOnClickOutside">
<div v-on-click-outside="closeDropdownOnClickOutside" class="dropdown-wrapper">
<div class="dropdown-overlay" :class="isDropdownExpandedClass" />
<button
class="mobile-button"
Expand Down
10 changes: 10 additions & 0 deletions src/stories/FiltersDropdown.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
describe('FiltersDropdown', () => {
it('Default', () => {
cy.visit(
'/iframe.html?id=filters-dropdown--default&args=&viewMode=story'
)
cy.get('.filters-dropdown').should('exist')

cy.percySnapshot('FiltersDropdown: Default')
})
})
116 changes: 116 additions & 0 deletions src/stories/FiltersDropdown.stories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { computed, onMounted, ref } from 'vue'
import FiltersDropdown from '@/lib-components/FiltersDropdown.vue'

export default {
title: 'Filters Dropdown',
component: FiltersDropdown,
}

// MOCK DATA
const mockFilterGroups = [
{
name: 'Event Type',
searchField: 'ftvaEventTypeFilters.title.keyword',
options: ['Film', 'Theater', 'Lecture'],
},
{
name: 'Screen Format',
searchField: 'ftvaScreeningFormatFilters.title.keyword',
options: ['Online', 'In-Person'],
},
]
// note that because this component uses v-model by default,
// parent component needs to use a ref for selectedFilters
const mockEmptySelectedFilters = ref({ 'ftvaEventTypeFilters.title.keyword': [], 'ftvaScreeningFormatFilters.title.keyword': [] })
const mockSelectedFilters = ref({
'ftvaEventTypeFilters.title.keyword': ['Film', 'Theater'],
'ftvaScreeningFormatFilters.title.keyword': ['Online'],
})

export function Default() {
return {
components: { FiltersDropdown },
data() {
return { mockFilterGroups, mockSelectedFilters: mockEmptySelectedFilters }
},
template: '<div style="width:400px"><span>Selected filters display:{{ mockSelectedFilters }}</span><filters-dropdown v-model:selectedFilters="mockSelectedFilters" :filterGroups="mockFilterGroups" /></div>',
}
}

// uses async data
export function InitialSelectedFilters() {
return {
components: { FiltersDropdown },
setup() {
const selectedFilters = ref({})
// mock getting selected filters from a route or other async source
const fetchFilters = async () => {
// Mocking an async fetch call
const response = await new Promise((resolve) => {
setTimeout(() => {
resolve(mockSelectedFilters.value)
}, 1000)
})
selectedFilters.value = response
}

fetchFilters()

return { selectedFilters }
},
data() {
return { mockFilterGroups }
},
template: '<span>Selected filters display:{{ selectedFilters }}</span><filters-dropdown v-model:selectedFilters="selectedFilters" :filterGroups="mockFilterGroups" />',
}
}

// FTVA Theme
export function FTVA() {
return {
components: { FiltersDropdown },
data() {
return { mockFilterGroups, mockSelectedFilters }
},
provide() {
return {
theme: computed(() => 'ftva'),
}
},
template: '<span>Selected filters display:{{ mockSelectedFilters }}</span><filters-dropdown v-model:selectedFilters="mockSelectedFilters" :filterGroups="mockFilterGroups" />',
}
}

// FTVA Theme W Selected Filters updating on 'done' click only
// This is the current planned implmentation on the FTVA site
const mockSelectedFiltersEmitted = ref({
'ftvaEventTypeFilters.title.keyword': ['Film'],
'ftvaScreeningFormatFilters.title.keyword': ['Online'],
})
export function FTVAFiltersUpdateDoneClick() {
return {
components: { FiltersDropdown },
setup() {
const selectedFiltersDisplay = ref({})

const updateFiltersDisplay = () => {
// assignment is done with spread operator so that a copy is made
selectedFiltersDisplay.value = { ...mockSelectedFiltersEmitted.value }
}
onMounted(() => {
// trigger function once onMount to update display with initial selected filters
updateFiltersDisplay()
})
return { selectedFiltersDisplay, updateFiltersDisplay }
},
data() {
return { mockFilterGroups, mockSelectedFiltersEmitted }
},
provide() {
return {
theme: computed(() => 'ftva'),
}
},
template: '<span>Selected filters display:{{ selectedFiltersDisplay }}</span><filters-dropdown v-model:selectedFilters="mockSelectedFiltersEmitted" @update-display="updateFiltersDisplay" :filterGroups="mockFilterGroups" />',
}
}
Loading
Loading