Skip to content

Commit 091a265

Browse files
authored
Merge pull request #4155 from nextcloud/enh/sticky-shadow
Some better visual feedback for sticky headers
2 parents f47ab1d + 16f80c0 commit 091a265

7 files changed

Lines changed: 77 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ All notable changes to this project will be documented in this file.
1212
- Added a forbidden route and page
1313
- Added WatchController to OCS-API
1414
- Sticky option headers in vote table
15-
- added a variant of the sticky option headers to the user settings
1615
- Lazy loading of participants on scroll, if too many vote cells are rendered
1716
- Support Nextcloud 30
1817

src/assets/scss/globals.scss

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,40 @@
99
}
1010

1111
.sticky-top {
12+
--shadow-height: 10px;
1213
position: sticky;
1314
top: 0;
1415
z-index: 4;
16+
padding-bottom: 0px;
17+
margin-bottom: var(--shadow-height);
18+
19+
&::after {
20+
content: '';
21+
position: absolute;
22+
bottom: 0;
23+
left: -1px;
24+
right: 0;
25+
height: 0px;
26+
background: linear-gradient(
27+
to bottom,
28+
rgba(var(--color-box-shadow-rgb), 0.3),
29+
rgba(var(--color-box-shadow-rgb), 0)
30+
);
31+
transition:
32+
all var(--animation-slow) linear,
33+
border 1ms;
34+
}
35+
36+
&.sticky-bottom-shadow {
37+
border-top: 0;
38+
padding-bottom: var(--shadow-height);
39+
margin-bottom: 0;
40+
&::after {
41+
height: var(--shadow-height);
42+
}
43+
}
44+
}
45+
46+
.sticky-top.sticky-left {
47+
z-index: 6;
1548
}

src/components/Base/modules/HeaderBar.vue

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,19 @@ function toggleClamp() {
4242
display: none;
4343
}
4444
45-
.scrolled .header_bar {
46-
box-shadow: 6px 6px 6px var(--color-box-shadow);
47-
}
48-
4945
.header_bar {
5046
position: sticky;
5147
top: 0;
5248
margin-inline: -8px;
5349
padding-inline: 56px 8px;
5450
background-color: var(--color-main-background);
55-
border-bottom: 1px solid var(--color-border);
5651
z-index: 9;
5752
transition: all var(--animation-slow) linear;
5853
54+
&::after {
55+
border-top: 1px solid var(--color-border);
56+
}
57+
5958
.header_bar_top {
6059
display: flex;
6160
flex-wrap: wrap-reverse;

src/components/Base/modules/IntersectionObserver.vue

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,25 @@
77
import { onBeforeUnmount, onMounted, ref } from 'vue'
88
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
99
10-
const inViewport = ref(false)
10+
// const model = ref(false)
11+
const model = defineModel<boolean>()
12+
1113
const observer = ref<null | IntersectionObserver>(null)
1214
1315
const observerTarget = ref<null | Element>(null)
14-
const emit = defineEmits(['visible'])
16+
const emit = defineEmits(['visible', 'invisible'])
1517
16-
const { loading } = defineProps<{ loading: boolean }>()
18+
const { loading = false } = defineProps<{ loading?: boolean }>()
1719
1820
onMounted(() => {
1921
const observer = new IntersectionObserver((entries) => {
2022
entries.forEach((entry) => {
2123
if (entry.isIntersecting) {
22-
inViewport.value = true
24+
model.value = true
2325
emit('visible')
2426
} else {
25-
inViewport.value = false
27+
model.value = false
28+
emit('invisible')
2629
}
2730
})
2831
})
@@ -40,6 +43,6 @@ onBeforeUnmount(() => {
4043
<template>
4144
<div ref="observerTarget">
4245
<NcLoadingIcon v-if="loading" :size="15" />
43-
<slot v-else :in-viewport="inViewport" />
46+
<slot v-else :in-viewport="model" />
4447
</div>
4548
</template>

src/components/Modals/TransferPollDialog.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const model = defineModel<boolean>({ required: true })
2222
2323
const pollsStore = usePollsStore()
2424
const pollStore = usePollStore()
25-
const newUser = ref<User | null>(null)
25+
const newUser = ref<User | undefined>(undefined)
2626
2727
async function dialogOK() {
2828
try {

src/components/VoteTable/VoteTable.vue

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,15 @@ import VoteParticipant from './VoteParticipant.vue'
2626
import IntersectionObserver from '../Base/modules/IntersectionObserver.vue'
2727
import { showError } from '@nextcloud/dialogs'
2828
29+
const emit = defineEmits(['headerSticky', 'headerUnSticky'])
30+
2931
const pollStore = usePollStore()
3032
const optionsStore = useOptionsStore()
3133
const votesStore = useVotesStore()
3234
const preferencesStore = usePreferencesStore()
35+
36+
const downPage = defineModel<boolean>('downPage', { default: false })
37+
3338
const chunksLoading = ref(false)
3439
3540
const tableStyle = computed(() => ({
@@ -63,7 +68,14 @@ function loadMore() {
6368
</script>
6469

6570
<template>
71+
<IntersectionObserver
72+
key="top-observer"
73+
v-model="downPage"
74+
@visible="emit('headerSticky')"
75+
@invisible="emit('headerUnSticky')" />
76+
6677
<TransitionGroup
78+
id="vote-table"
6779
tag="div"
6880
name="list"
6981
:class="pollStore.viewMode"
@@ -88,7 +100,8 @@ function loadMore() {
88100
<div
89101
v-if="pollStore.viewMode === ViewMode.TableView"
90102
key="option-spacer"
91-
class="option-spacer sticky-left" />
103+
class="option-spacer sticky-left sticky-top"
104+
:class="{ 'sticky-bottom-shadow': !downPage }" />
92105
<div
93106
v-if="pollStore.permissions.seeResults"
94107
class="counter-spacer sticky-left" />
@@ -122,6 +135,7 @@ function loadMore() {
122135
<OptionItem
123136
:id="`option-${option.id}`"
124137
class="sticky-top"
138+
:class="{ 'sticky-bottom-shadow': !downPage }"
125139
:option="option" />
126140
<Counter
127141
v-if="pollStore.permissions.seeResults"
@@ -149,7 +163,7 @@ function loadMore() {
149163
"
150164
class="observer-container sticky-left">
151165
<IntersectionObserver
152-
key="observer"
166+
key="bottom-observer"
153167
class="observer_section"
154168
:loading="chunksLoading"
155169
@visible="loadMore">
@@ -269,7 +283,6 @@ function loadMore() {
269283
270284
.option-item {
271285
grid-row: 2;
272-
opacity: 0.85;
273286
background-color: var(--color-main-background);
274287
border-inline-start: 1px solid var(--color-border);
275288
}

src/views/Vote.vue

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
-->
55

66
<script setup lang="ts">
7-
import { computed, onMounted, onUnmounted } from 'vue'
7+
import { computed, onMounted, onUnmounted, ref } from 'vue'
88
import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'
99
import { t } from '@nextcloud/l10n'
1010
@@ -14,7 +14,6 @@ import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
1414
import DatePollIcon from 'vue-material-design-icons/CalendarBlank.vue'
1515
import TextPollIcon from 'vue-material-design-icons/FormatListBulletedSquare.vue'
1616
17-
import { useHandleScroll } from '../composables/handleScroll.ts'
1817
import MarkDownDescription from '../components/Poll/MarkDownDescription.vue'
1918
import ActionAddOption from '../components/Actions/modules/ActionAddOption.vue'
2019
import PollInfoLine from '../components/Poll/PollInfoLine.vue'
@@ -34,12 +33,14 @@ import { Event } from '../Types/index.ts'
3433
import Collapsible from '../components/Base/modules/Collapsible.vue'
3534
import type { CollapsibleProps } from '../components/Base/modules/Collapsible.vue'
3635
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
36+
import IntersectionObserver from '../components/Base/modules/IntersectionObserver.vue'
3737
3838
const pollStore = usePollStore()
3939
const optionsStore = useOptionsStore()
4040
const preferencesStore = usePreferencesStore()
4141
const voteMainId = 'watched-scroll-area'
42-
const scrolled = useHandleScroll(voteMainId)
42+
const topObserverVisible = ref(false)
43+
const voteHeaderDownPage = ref(false)
4344
4445
const loadingOverlayProps = {
4546
name: t('polls', 'Loading poll…'),
@@ -94,6 +95,10 @@ const collapsibleProps = computed<CollapsibleProps>(() => ({
9495
initialState: pollStore.currentUserStatus.countVotes === 0 ? 'max' : 'min',
9596
}))
9697
98+
const scrolled = computed(
99+
() => !topObserverVisible.value && voteHeaderDownPage.value,
100+
)
101+
97102
onBeforeRouteUpdate(async () => {
98103
pollStore.load()
99104
emit(Event.TransitionsOff, 500)
@@ -122,11 +127,11 @@ onUnmounted(() => {
122127
pollStore.viewMode,
123128
voteMainId,
124129
{
125-
scrolled: scrolled,
130+
scrolled,
126131
'vote-style-beta-510': preferencesStore.user.useAlternativeStyling,
127132
},
128133
]">
129-
<HeaderBar>
134+
<HeaderBar class="sticky-top" :class="{ 'sticky-bottom-shadow': scrolled }">
130135
<template #title>
131136
{{ pollStore.configuration.title }}
132137
</template>
@@ -139,6 +144,7 @@ onUnmounted(() => {
139144
</HeaderBar>
140145

141146
<div class="vote_main">
147+
<IntersectionObserver id="top-observer" v-model="topObserverVisible" />
142148
<Collapsible
143149
v-if="pollStore.configuration.description"
144150
class="sticky-left"
@@ -148,7 +154,9 @@ onUnmounted(() => {
148154

149155
<VoteInfoCards class="sticky-left" />
150156

151-
<VoteTable v-show="optionsStore.options.length" />
157+
<VoteTable
158+
v-show="optionsStore.options.length"
159+
v-model:down-page="voteHeaderDownPage" />
152160

153161
<NcEmptyContent
154162
v-if="!optionsStore.options.length"

0 commit comments

Comments
 (0)