kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
feat(front): make pagination responsive and accessible
https://benmyers.dev/blog/native-visually-hidden/2506-fix-frontend-regressions
rodzic
30583f4a53
commit
26fd4b575d
|
@ -3,6 +3,7 @@ import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
|||
import { useI18n } from 'vue-i18n'
|
||||
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
|
||||
import { type ColorProps, type VariantProps, type DefaultProps, type RaisedProps, type PastelProps, color } from '~/composables/color.ts'
|
||||
import { type WidthProps, width } from '~/composables/width'
|
||||
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import Layout from '~/components/ui/Layout.vue'
|
||||
|
@ -18,7 +19,9 @@ const { icon, placeholder, ...props } = defineProps<{
|
|||
reset?:() => void;
|
||||
} & (ColorProps | DefaultProps | PastelProps)
|
||||
& VariantProps
|
||||
& RaisedProps>()
|
||||
& RaisedProps
|
||||
& WidthProps
|
||||
>()
|
||||
|
||||
// TODO(A11y): Add `inputmode="numeric" pattern="[0-9]*"` to input if model type is number:
|
||||
// https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/
|
||||
|
@ -79,7 +82,7 @@ const model = defineModel<string|number>({ required: true })
|
|||
</span>
|
||||
|
||||
<input
|
||||
v-bind="{...$attrs, ...attributes, ...color(props, ['solid', 'default', 'secondary'])()}"
|
||||
v-bind="{...$attrs, ...attributes, ...color(props, ['solid', 'default', 'secondary'])(width(props)())}"
|
||||
ref="input"
|
||||
v-model="model"
|
||||
:autofocus="autofocus || undefined"
|
||||
|
|
|
@ -8,7 +8,7 @@ const props = defineProps<{
|
|||
noRule?: true,
|
||||
noWrap?: true,
|
||||
} & { [P in 'stack' | 'grid' | 'flex' | 'columns' | 'row' | 'page']?: true | string }
|
||||
& { [C in 'nav' | 'aside' | 'header' | 'footer' | 'main' | 'label' | 'form' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5']?: true }
|
||||
& { [C in 'nav' | 'ul' | 'aside' | 'header' | 'footer' | 'main' | 'label' | 'form' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5']?: true }
|
||||
& { [G in 'no-gap' | `gap-${'4' | '8' | '12' | '16' | '24' | '32' | '48' | '64' | 'auto'}` ]?: true }
|
||||
&(PastelProps | ColorProps | DefaultProps)
|
||||
& RaisedProps
|
||||
|
@ -38,7 +38,7 @@ const attributes = computed(() => ({
|
|||
|
||||
<template>
|
||||
<component
|
||||
:is="props.nav ? 'nav' : props.aside ? 'aside' : props.header ? 'header' : props.footer ? 'footer' : props.main ? 'main' : props.label ? 'label' : props.form ? 'form' : props.h1 ? 'h1' : props.h2 ? 'h2' : props.h3 ? 'h3' : props.h4 ? 'h4' : props.h5 ? 'h5' : 'div'"
|
||||
:is="props.nav ? 'nav' : props.ul ? 'ul' : props.aside ? 'aside' : props.header ? 'header' : props.footer ? 'footer' : props.main ? 'main' : props.label ? 'label' : props.form ? 'form' : props.h1 ? 'h1' : props.h2 ? 'h2' : props.h3 ? 'h3' : props.h4 ? 'h4' : props.h5 ? 'h5' : 'div'"
|
||||
:class="[
|
||||
$style.layout,
|
||||
('noGap' in props && props.noGap === true) || $style.gap,
|
||||
|
|
|
@ -6,6 +6,7 @@ import { isMobileView } from '~/composables/screen'
|
|||
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import Input from '~/components/ui/Input.vue'
|
||||
import Spacer from '~/components/ui/Spacer.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
@ -23,7 +24,11 @@ const goTo = ref<number | string>('' as const)
|
|||
|
||||
const range = (start: number, end: number) => Array.from({ length: end - start + 1 }, (_, i) => i + start)
|
||||
|
||||
/* Why? What? */
|
||||
/* Render only pages nearby if >5 pages:
|
||||
- If near the end, show 1, ..., end-4, end-3, end-2, end-1, end
|
||||
- If smaller than -4 and larger than 4, show 1, ..., current-1, current, current+1, ..., end
|
||||
- If near beginning, show 1, 2, 3, 4, 5, ..., 12
|
||||
*/
|
||||
const renderPages = computed(() => {
|
||||
const start = range(2, 5)
|
||||
const end = range(pages - 4, pages - 1)
|
||||
|
@ -72,6 +77,11 @@ watch(page, (_) => {
|
|||
})
|
||||
</script>
|
||||
|
||||
|
||||
<!-- REDESIGN, without custom CSS -->
|
||||
|
||||
|
||||
|
||||
<template>
|
||||
<nav
|
||||
ref="pagination"
|
||||
|
@ -85,78 +95,66 @@ watch(page, (_) => {
|
|||
<Button
|
||||
low-height
|
||||
min-content
|
||||
:square-small="isSmall"
|
||||
:disabled="page <= 1"
|
||||
:aria-label="t('vui.aria.pagination.gotoPrevious')"
|
||||
secondary
|
||||
ghost
|
||||
icon="bi-chevron-left"
|
||||
class="visually-hidden-when-small"
|
||||
@click="page -= 1"
|
||||
>
|
||||
<span v-if="!isSmall">{{ t('vui.pagination.previous') }}</span>
|
||||
</Button>
|
||||
</li>
|
||||
|
||||
<Spacer
|
||||
no-size
|
||||
grow
|
||||
/>
|
||||
|
||||
<template
|
||||
v-for="(i, index) in (isSmall ? [] : renderPages)"
|
||||
v-for="(i, index) in (renderPages)"
|
||||
:key="i"
|
||||
>
|
||||
<li>
|
||||
<Button
|
||||
v-if="i <= pages && i > 0 && pages > 3"
|
||||
v-if="i <= pages && i > 0 && pages > 2"
|
||||
square-small
|
||||
:aria-label="page !== i ? t('vui.aria.pagination.gotoPage', i) : t('vui.aria.pagination.currentPage', page)"
|
||||
:secondary="page !== i"
|
||||
:aria-pressed="page === i"
|
||||
circular
|
||||
ghost
|
||||
@click="page = i"
|
||||
>
|
||||
{{ i }}
|
||||
</Button>
|
||||
</li>
|
||||
<li v-if="i + 1 < renderPages[index + 1]">
|
||||
{{ (() => '…')() }}
|
||||
</li>
|
||||
</template>
|
||||
<template v-if="isSmall">
|
||||
<li>
|
||||
<Button
|
||||
square-small
|
||||
:aria-label="page !== 1 ? t('vui.aria.pagination.gotoPage', page) : t('vui.aria.pagination.currentPage', page)"
|
||||
:secondary="page !== 1"
|
||||
@click="page = 1"
|
||||
>
|
||||
{{ (() => '1')() }}
|
||||
</Button>
|
||||
</li>
|
||||
<li v-if="page === 1 || page === pages">
|
||||
{{ (() => '…')() }}
|
||||
</li>
|
||||
<li v-else>
|
||||
<Button
|
||||
square-small
|
||||
:aria-label="t('vui.aria.pagination.currentPage', page)"
|
||||
aria-current="true"
|
||||
>
|
||||
{{ page }}
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button
|
||||
square-small
|
||||
:aria-label="page !== pages ? t('vui.aria.pagination.gotoPage', page) : t('vui.aria.pagination.currentPage', page)"
|
||||
:secondary="page !== pages"
|
||||
@click="page = pages"
|
||||
>
|
||||
{{ pages }}
|
||||
</Button>
|
||||
<li
|
||||
v-if="i + 1 < renderPages[index + 1]"
|
||||
style="user-select: none;"
|
||||
>
|
||||
{{ t('vui.pagination.ellipsis') }}
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<Spacer
|
||||
no-size
|
||||
grow
|
||||
/>
|
||||
|
||||
<li>
|
||||
<Button
|
||||
low-height
|
||||
min-content
|
||||
:square-small="isSmall"
|
||||
:disabled="page >= pages"
|
||||
:aria-label="t('vui.aria.pagination.gotoNext')"
|
||||
secondary
|
||||
ghost
|
||||
icon="right bi-chevron-right"
|
||||
class="visually-hidden-when-small"
|
||||
@click="page += 1"
|
||||
>
|
||||
<span v-if="!isSmall">{{ t('vui.pagination.next') }}</span>
|
||||
|
@ -164,17 +162,23 @@ watch(page, (_) => {
|
|||
</li>
|
||||
</ul>
|
||||
<!-- \d{1,100} -->
|
||||
<div class="goto">
|
||||
{{ t('vui.go-to') }}
|
||||
<Spacer size-8 />
|
||||
<label
|
||||
style="transform: translateY(-7px);"
|
||||
>
|
||||
<Input
|
||||
v-model.number="goTo"
|
||||
:placeholder="page?.toString()"
|
||||
:placeholder="t('vui.pagination.enterPageNumber')"
|
||||
low-height
|
||||
tiny
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
:aria-label="t('vui.aria.pagination.goToPage', { goTo })"
|
||||
@click.stop
|
||||
@keyup.enter="setPage"
|
||||
@blur="setPage"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,20 +1,27 @@
|
|||
.funkwhale {
|
||||
&.pagination {
|
||||
|
||||
@include light-theme {
|
||||
> .goto {
|
||||
border-left: 1px solid var(--fw-gray-200);
|
||||
}
|
||||
}
|
||||
/* People using a screen reader want to be able to use default 'previous page', 'next page' controls*/
|
||||
|
||||
@include dark-theme {
|
||||
> .goto {
|
||||
border-left: 1px solid var(--fw-gray-800);
|
||||
}
|
||||
@media screen and (max-width: 671px) {
|
||||
flex-wrap: wrap;
|
||||
.visually-hidden-when-small:not(:focus):not(:active) {
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
height: 34px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&.is-small {
|
||||
> .pages {
|
||||
|
@ -33,46 +40,23 @@
|
|||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
> li {
|
||||
margin-top: 0;
|
||||
margin: 0 0.5ch;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
|
||||
&:not(:first-child):not(:last-child) {
|
||||
width: 34px;
|
||||
}
|
||||
|
||||
> .funkwhale.button {
|
||||
min-width: 34px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
&:first-child > .funkwhale.button,
|
||||
&:last-child > .funkwhale.button {
|
||||
min-width: 94px;
|
||||
width: 94px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .goto {
|
||||
margin-left: 16px;
|
||||
padding-left: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
|
||||
> .funkwhale.input {
|
||||
margin-left: 16px;
|
||||
width: calc(3ch + 32px);
|
||||
|
||||
input {
|
||||
text-align: center;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ export type WidthProps =
|
|||
| { squareSmall?: true }
|
||||
| { lowHeight?: true }
|
||||
| { normalHeight?: true }
|
||||
| { circular? : true }
|
||||
export type Key = KeysOfUnion<WidthProps>
|
||||
|
||||
const widths = {
|
||||
|
@ -30,6 +31,7 @@ const widths = {
|
|||
auto: 'width: auto;',
|
||||
full: 'width: auto; --grid-column: 1 / -1; place-self: stretch;',
|
||||
grow: 'flex-grow: 1;',
|
||||
circular: 'border-radius: 100%;',
|
||||
width: (w: string) => `width: ${w}; flex-grow:0;`
|
||||
} as const
|
||||
|
||||
|
|
|
@ -4689,25 +4689,55 @@
|
|||
}
|
||||
},
|
||||
"vui": {
|
||||
"albums": "{n} album | {n} albums",
|
||||
"aria": {
|
||||
"pagination": {
|
||||
"currentPage": "Current Page, Page {n}",
|
||||
"gotoNext": "Goto Next Page",
|
||||
"gotoPage": "Goto Page {n}",
|
||||
"gotoPrevious": "Goto Previous Page",
|
||||
"nav": "Pagination Navigation"
|
||||
}
|
||||
},
|
||||
"by-user": "by {username}",
|
||||
"episodes": "{n} episode | {n} episodes",
|
||||
"go-to": "Go to",
|
||||
"pagination": {
|
||||
"label": "Pagination",
|
||||
"next": "Next page",
|
||||
"previous": "Previous page"
|
||||
},
|
||||
"addItem": "Add {item}",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"deselect": "Deselect",
|
||||
"deletItem": "Delete {item}",
|
||||
"pressKey": "Press {key}",
|
||||
"pressKeyToAction": "Press {key} to {action}",
|
||||
"preview": "Preview",
|
||||
"resetTo": "Reset to {previousValue}",
|
||||
"radio": "Radio",
|
||||
"tracks": "{n} track | {n} tracks"
|
||||
"albums": "{n} album | {n} albums",
|
||||
"tracks": "{n} track | {n} tracks",
|
||||
"episodes": "{n} episode | {n} episodes",
|
||||
"by-user": "by {username}",
|
||||
"pagination": {
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"ellipsis": "…",
|
||||
"enterPageNumber": "Go to page…"
|
||||
},
|
||||
"aria": {
|
||||
"close": "Close",
|
||||
"text": {
|
||||
"paragraph": "paragraph",
|
||||
"heading1": "heading1",
|
||||
"heading2": "heading2",
|
||||
"heading3": "heading3",
|
||||
"heading4": "heading4",
|
||||
"heading5": "heading5",
|
||||
"heading6": "heading6",
|
||||
"quote": "quote",
|
||||
"orderedList": "ordered list",
|
||||
"unorderedList": "unordered list",
|
||||
"bold": "bold",
|
||||
"italic": "italic",
|
||||
"strikethrough": "strikethrough",
|
||||
"link": "link"
|
||||
},
|
||||
"password": {
|
||||
"show": "Show password",
|
||||
"hide": "Hide password"
|
||||
},
|
||||
"pagination": {
|
||||
"nav": "Pagination Navigation",
|
||||
"goToPage": "Go to Page {n}",
|
||||
"gotoPrevious": "Go to Previous Page",
|
||||
"gotoNext": "Go to Next Page",
|
||||
"currentPage": "Current Page, Page {n}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,10 +17,11 @@
|
|||
"tracks": "{n} track | {n} tracks",
|
||||
"episodes": "{n} episode | {n} episodes",
|
||||
"by-user": "by {username}",
|
||||
"go-to": "Go to",
|
||||
"pagination": {
|
||||
"previous": "Previous",
|
||||
"next": "Next"
|
||||
"next": "Next",
|
||||
"ellipsis": "…",
|
||||
"enterPageNumber": "Go to page…"
|
||||
},
|
||||
"aria": {
|
||||
"close": "Close",
|
||||
|
@ -46,7 +47,7 @@
|
|||
},
|
||||
"pagination": {
|
||||
"nav": "Pagination Navigation",
|
||||
"gotoPage": "Go to Page {n}",
|
||||
"goToPage": "Go to Page {n}",
|
||||
"gotoPrevious": "Go to Previous Page",
|
||||
"gotoNext": "Go to Next Page",
|
||||
"currentPage": "Current Page, Page {n}"
|
||||
|
|
|
@ -632,9 +632,9 @@
|
|||
background-color: var(--active-background-color);
|
||||
}
|
||||
&[disabled] {
|
||||
color: var(--disabled-color);
|
||||
border-color: var(--disabled-border-color);
|
||||
background-color:var(--disabled-background-color);
|
||||
color: color-mix(in oklab, var(--disabled-color) 50%, var(--color-over-transparent, var(--color)));
|
||||
border-color: transparent;
|
||||
background-color: transparent;
|
||||
}
|
||||
// Link
|
||||
/* &.active {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
.ui.bottom-player {
|
||||
z-index: 999999;
|
||||
width: 100%;
|
||||
|
@ -14,9 +13,9 @@
|
|||
height: 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.ui.bottom-player > .segment.fixed-controls {
|
||||
max-width: 100vw;
|
||||
color: var(--color);
|
||||
background: var(--player-background);
|
||||
width: 100%;
|
||||
|
@ -147,14 +146,16 @@
|
|||
accent-color: var(--vibrant-color);
|
||||
}
|
||||
|
||||
.looping, .shuffling,
|
||||
.looping:hover, .shuffling:hover {
|
||||
.looping,
|
||||
.shuffling,
|
||||
.looping:hover,
|
||||
.shuffling:hover {
|
||||
> i {
|
||||
color: var(--vibrant-color);
|
||||
}
|
||||
}
|
||||
|
||||
@include media(">desktop") {
|
||||
@include media('>desktop') {
|
||||
&:not(.fluid) {
|
||||
width: 20%;
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue