-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Apps-3035 create filters drop down component (#664)
* feat: filtersdropdown scaffolding * feat: filters component and stories * chore: comment in progress work * chore: add types * feat: styles * chore: cleanup, add async data test * chore: lint * feat: add emits and story * feat: add emits and focus styles --------- Co-authored-by: Jess Divers <[email protected]>
- Loading branch information
1 parent
90fdeab
commit 8dbad4a
Showing
8 changed files
with
415 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" />', | ||
} | ||
} |
Oops, something went wrong.
8dbad4a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉 Published on https://ucla-library-storybook.netlify.app as production
🚀 Deployed on https://675b91a08944e548ea790302--ucla-library-storybook.netlify.app