UCLALibrary / ucla-library-website-components

This is a library of Vue components that will be used in UCLA Library Nuxt websites.
Other
6 stars 1 forks source link

Component Request - FiltersDropDown #652

Open jendiamond opened 1 week ago

jendiamond commented 1 week ago

Component Description

This component is used at the top of the the Event Listing page that has a filtered search. It will display; what the data is to be filtered by.


Screenshot 2024-11-13 at 11 09 59 AM

Further Discussion

Question: Can we use BlockTag in this dropdown component?

OverView

Listing View

Screenshot 2024-11-13 at 10 29 28 AM

Calendar view

Mobile view


Design

For quick reference.

DESKTOP:

https://www.figma.com/design/EKazRIMP4B15bD16UDbOwR/UCLA-Library-Design-System?node-id=237-7359&node-type=canvas&t=IAIXRJOa9cSjuUke-0

Screenshot 2024-11-12 at 4 27 23 PM Screenshot 2024-11-12 at 4 14 31 PM Screenshot 2024-11-12 at 4 27 51 PM Screenshot 2024-11-12 at 4 28 33 PM

TABLET

To be determined Possibly the List view & Calendar view are only icons

Screenshot 2024-11-13 at 10 31 41 AM

MOBILE:

Apply a Horizontal scroll to the filters https://www.figma.com/file/ZT2qWKTlOxfhr1QUS2rFPL/UI-Pattern-Library-(Client-Facing)-Final?node-id=7%3A58

Screenshot 2024-11-12 at 1 11 09 PM Screenshot 2024-11-12 at 4 12 58 PM Screenshot 2024-11-12 at 4 13 33 PM

Slots

The Drawer component will use a slot where the mobile view of the filters will go.

Props

filterGroups: {
    type: Array as PropType<FilterGroupsTypes[]>,
    default: () => [],
  },
selectedFilters:{ // This is like initialDates prop of DateFilter
   type: Object as PropType<SelectedFiltersTypes>,
    default: () => {},
}

Developer Tips:

Vue SFC Playground link

Sample usage on Nuxt Page:

<template>
  <DropdownFilter
    :filterGroups="filterGroups"
    :selectedFilters="selectedFilters"
@input-selected="doSearch"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue';

const filterGroups = [
  {
    name: 'Event Type',
    searchField: 'ftvaEventTypeFilters.title.keyword',
    options: ['Film', 'Theater', 'Lecture'],
  },
  {
    name: 'Screen Format',
    searchField: 'ftvaScreeningFormatFilters.title.keyword',
    options: ['Online', 'In-Person'],
  },
];

const selectedFilters = ref({});
</script>

Sample Data structure for selectedFilters props(This is like initialDates prop of DateFilter)

{
  ftvaEventTypeFilters.title.keyword: ['Film', 'Theater'],
  ftvaScreeningFormatFilters.title.keyword: ['Online'],
}

Component Positioning:

Align the BlockTag component visually in the same place as the hidden checkbox for accessible interaction.

Responsive Design:

Ensure pills are responsive, wrapping onto new lines as necessary on smaller screens.

Accessibility:

Styling Details:

Sample DropDownFilters code

<MobileDrawer>
      <template #buttonLabel>
        <!-- Optional Button Icon -->
        <div class="filter-summary">Filters ({{ totalFiltersCount }})</div>

<span v-if="hasIcon" class="icon-svg">
          <component :is="SvgIconFtvaFiltersSample" class="button-svg" aria-hidden="true" />
        </span>
      </template>
      <template #dropdownItems>
<div class="dropdown-filter">
    <div v-for="group in filterGroups" :key="group.name" class="filter-group">
      <h3>{{ group.name }}</h3>
      <div class="pills">
        <label
          v-for="option in group.options"
          :key="option"
          class="pill-label"
        >
          <!-- Hidden checkbox for managing selection -->

        <input
            type="checkbox"
            class="pill-checkbox"
            :value="option"
            :id="option"
            :checked="isSelected(group.searchField, option)"
            @change="toggleSelection(group.searchField, option)"

          />
          <!-- BlockTag component for display, positioned over the checkbox -->
          <BlockTag
            :label="option"
            :isSecondary="false"
          >
            <!-- 'x' SVG only shows when selected -->
            <template v-if="isSelected(group.searchField, option)">
              <SvgGlyphX class="x-icon" />
            </template>
          </BlockTag>
        </label>
      </div>
    </div>
    <button @click="applyFilters">Done</button>
<button @click="clearFilters">Clear</button>

  </div>
</template>
</mobileDrawer>

Sample script setup code from VueSFC playground

// Emit events
const emit = defineEmits(['update:selectedFilters', 'input-selected']);

// Copy of selected filters for local state
const selectedFiltersCopy = ref({ ...props.selectedFilters });

// Watch for changes in props.selectedFilters
watch(
  () => props.selectedFilters,
  (newQueryFilters) => {
    console.log(
      'In watch function props.selectedFilters updated',
      JSON.stringify(newQueryFilters),
      JSON.stringify(props.filterGroups)
    );

    Object.entries(newQueryFilters).forEach(([key, value]) => {
      selectedFiltersCopy.value[key] = value;
    });

    console.log('selectedFiltersCopy.value', JSON.stringify(selectedFiltersCopy.value));
  },
  { deep: true }
);

// Check if option is selected
const isSelected = (searchField: string, option: string) => {
  return selectedFiltersCopy.value[searchField]?.includes(option) ?? false;
};

// Function to toggle filter selection
const toggleSelection = (searchField: string, option: string) => {
  if (!selectedFiltersCopy.value[searchField]) {
    selectedFiltersCopy.value[searchField] = [];
  }

  if (selectedFiltersCopy.value[searchField].includes(option)) {
    selectedFiltersCopy.value[searchField] = selectedFiltersCopy.value[searchField].filter(
      (item) => item !== option
    );
  } else {
    selectedFiltersCopy.value[searchField].push(option);
  }
};

// Emit selected filters to parent when "Done" is clicked
const applyFilters = () => {
  emit('input-selected', selectedFiltersCopy.value);
};

// Clear filters logic
const clearFilters = () => {
  // Reset each filter group to an empty array
  props.filterGroups.forEach((group) => {
    selectedFiltersCopy.value[group.searchField] = [];
  });
 emit('input-selected', selectedFiltersCopy.value);
}

// Computed property for total selected filter count
const totalFiltersCount = computed(() =>
  Object.values(selectedFiltersCopy.value).reduce((count, options) => count + options.length, 0)
);

Sample styles:

<style scoped lang="scss">
.dropdown-filter {
  .pills {
    display: flex;
    flex-wrap: wrap;
  }

  .pill-label {
    display: inline-flex;
    align-items: center;
    position: relative;
  }

  /* Hide the checkbox visually but keep it for accessibility */
  .pill-checkbox {
    position: absolute;
    opacity: 0;
    pointer-events: none;
  }

  /* Position BlockTag in place of the checkbox */
  :deep(.block-tag) {
    cursor: pointer;

    position: relative;
    z-index: 1;
  }

  /* Hover and selected state styling */
  :deep(.block-tag:hover),
  .pill-checkbox:checked + :deep(.block-tag) {
    background-color: ; /* UCLA Blue */
    color: ;/* White */

    .label {
      color: ;
    }
  }

  /* 'x' icon styling */
  .x-icon {

  }
}
</style>
Build this component with typescript.

Events

input-selected:Payload: { [searchField: string]: string[] }
Emitted whenever the user clicks on Done option.

Child components

Components that are used by this new component.

  1. BlockTag https://ucla-library-storybook.netlify.app/?path=/story/search-block-remove-search-filter--ftva for the pills
  2. SVG import('ucla-library-design-tokens/assets/svgs/icon-ftva-xtag.svg')

Resources

Vue 3: Form Input checkbox Accessible Filters

Screenshots

{attach screenshots}