Sidebar refinements (#2306)

* Begin sidebar refinement, change back to left as default

* New filters proof of concept

* Hide if only one option

* Version filters

* Update changelog page

* Use new cosmetic variable for sidebar position

* Fix safari issue and change defaults to left filters, right sidebars

* Fix download modal on safari and firefox

* Add date published tooltip to versions page

* Improve selection consistency

* Fix lint and extract i18n

* Remove unnecessary observer options
This commit is contained in:
Prospector
2024-08-26 16:53:27 -07:00
committed by GitHub
parent 656c5b61cc
commit 2dd8d5a119
22 changed files with 965 additions and 779 deletions

View File

@@ -0,0 +1,116 @@
<template>
<ButtonStyled>
<PopoutMenu
v-if="options.length > 1"
v-bind="$attrs"
:disabled="disabled"
:position="position"
:direction="direction"
@open="() => {
searchQuery = ''
}"
>
<slot />
<DropdownIcon class="h-5 w-5 text-secondary" />
<template #menu>
<div class="iconified-input mb-2 w-full" v-if="search">
<label for="search-input" hidden>Search...</label>
<SearchIcon aria-hidden="true" />
<input id="search-input" v-model="searchQuery" placeholder="Search..." type="text" ref="searchInput" @keydown.enter="() => {
toggleOption(filteredOptions[0])
}" />
</div>
<ScrollablePanel
v-if="search" class="h-[17rem]">
<Button
v-for="(option, index) in filteredOptions"
:key="`option-${index}`"
:transparent="!manyValues.includes(option)"
:action="() => toggleOption(option)"
class="!w-full"
:color="manyValues.includes(option) ? 'secondary' : 'default'"
>
<slot name="option" :option="option">{{ displayName(option) }}</slot>
<CheckIcon class="h-5 w-5 text-contrast ml-auto transition-opacity" :class="{ 'opacity-0': !manyValues.includes(option) }" />
</Button>
</ScrollablePanel>
<div
v-else class="flex flex-col gap-1">
<Button
v-for="(option, index) in filteredOptions"
:key="`option-${index}`"
:transparent="!manyValues.includes(option)"
:action="() => toggleOption(option)"
class="!w-full"
:color="manyValues.includes(option) ? 'secondary' : 'default'"
>
<slot name="option" :option="option">{{ displayName(option) }}</slot>
<CheckIcon class="h-5 w-5 text-contrast ml-auto transition-opacity" :class="{ 'opacity-0': !manyValues.includes(option) }" />
</Button>
</div>
<slot name="footer" />
</template>
</PopoutMenu>
</ButtonStyled>
</template>
<script setup lang="ts">
import { CheckIcon, DropdownIcon, SearchIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, PopoutMenu, Button } from '../index'
import { computed, ref } from 'vue'
import ScrollablePanel from './ScrollablePanel.vue'
type Option = string | number | object
const props = withDefaults(
defineProps<{
modelValue: Option[],
options: Option[]
disabled?: boolean
position?: string
direction?: string,
displayName?: (option: Option) => string,
search?: boolean
}>(),
{
disabled: false,
position: 'auto',
direction: 'auto',
displayName: (option: Option) => option as string,
search: false,
},
)
const emit = defineEmits(['update:modelValue', 'change']);
const selectedValues = ref(props.modelValue || [])
const searchInput = ref();
const searchQuery = ref('')
const manyValues = computed({
get() {
return props.modelValue || selectedValues.value
},
set(newValue) {
emit('update:modelValue', newValue)
emit('change', newValue)
selectedValues.value = newValue
},
})
const filteredOptions = computed(() => {
return props.options.filter((x) => !searchQuery.value || props.displayName(x).toLowerCase().includes(searchQuery.value.toLowerCase()))
})
defineOptions({
inheritAttrs: false,
})
function toggleOption(id: Option) {
if (manyValues.value.includes(id)) {
manyValues.value = manyValues.value.filter((x) => x !== id)
} else {
manyValues.value = [...manyValues.value, id]
}
}
</script>

View File

@@ -4,7 +4,7 @@
v-bind="$attrs"
ref="dropdownButton"
:class="{ 'popout-open': dropdownVisible }"
tabindex="-1"
:tabindex="tabInto ? -1 : 0"
@click="toggleDropdown"
>
<slot></slot>
@@ -12,6 +12,7 @@
<div
class="popup-menu"
:class="`position-${computedPosition}-${computedDirection} ${dropdownVisible ? 'visible' : ''}`"
:inert="!tabInto && !dropdownVisible"
>
<slot name="menu"> </slot>
</div>
@@ -34,11 +35,17 @@ const props = defineProps({
type: String,
default: 'auto',
},
tabInto: {
type: Boolean,
default: false,
},
})
defineOptions({
inheritAttrs: false,
})
const emit = defineEmits(['open', 'close'])
const dropdownVisible = ref(false)
const dropdown = ref(null)
const dropdownButton = ref(null)
@@ -71,8 +78,11 @@ function updateDirection() {
const toggleDropdown = () => {
if (!props.disabled) {
dropdownVisible.value = !dropdownVisible.value
if (!dropdownVisible.value) {
if (dropdownVisible.value) {
emit('open')
} else {
dropdownButton.value.focus()
emit('close')
}
}
}
@@ -80,10 +90,12 @@ const toggleDropdown = () => {
const hide = () => {
dropdownVisible.value = false
dropdownButton.value.focus()
emit('close')
}
const show = () => {
dropdownVisible.value = true
emit('open')
}
defineExpose({
@@ -99,6 +111,7 @@ const handleClickOutside = (event) => {
!dropdown.value.contains(event.target)
) {
dropdownVisible.value = false
emit('close')
}
}
@@ -106,6 +119,7 @@ onMounted(() => {
window.addEventListener('click', handleClickOutside)
window.addEventListener('resize', updateDirection)
window.addEventListener('scroll', updateDirection)
window.addEventListener('keydown', handleKeyDown)
updateDirection()
})
@@ -113,7 +127,14 @@ onBeforeUnmount(() => {
window.removeEventListener('click', handleClickOutside)
window.removeEventListener('resize', updateDirection)
window.removeEventListener('scroll', updateDirection)
window.removeEventListener('keydown', handleKeyDown)
})
function handleKeyDown(event) {
if (event.key === 'Escape') {
hide()
}
}
</script>
<style lang="scss" scoped>

View File

@@ -107,7 +107,7 @@ function onScroll({ target: { scrollTop, offsetHeight, scrollHeight } }) {
.scrollable-pane {
display: flex;
flex-direction: column;
gap: 0.5rem;
gap: 0.25rem;
height: 100%;
width: 100%;
overflow-y: auto;

View File

@@ -13,6 +13,7 @@ export { default as DropArea } from './base/DropArea.vue'
export { default as DropdownSelect } from './base/DropdownSelect.vue'
export { default as EnvironmentIndicator } from './base/EnvironmentIndicator.vue'
export { default as FileInput } from './base/FileInput.vue'
export { default as ManySelect } from './base/ManySelect.vue'
export { default as MarkdownEditor } from './base/MarkdownEditor.vue'
export { default as Notifications } from './base/Notifications.vue'
export { default as OverflowMenu } from './base/OverflowMenu.vue'

View File

@@ -32,7 +32,6 @@ const messages = defineMessages({
<template>
<div
:style="`--_size: ${size};`"
:class="`flex ${large ? 'text-lg w-[2.625rem] h-[2.625rem]' : 'text-sm w-9 h-9'} font-bold justify-center items-center rounded-full ${channel === 'release' ? 'bg-bg-green text-brand-green' : channel === 'beta' ? 'bg-bg-orange text-brand-orange' : 'bg-bg-red text-brand-red'}`"
>
{{ channel ? formatMessage(messages[`${channel}Symbol`]) : '?' }}