A new way to subscribe (#4238)

* Implement styling for new subscribe button

* Implement dropdown element and styling

* Implements remaining styling and logic

* Correct use of aria-selected in FT

Note: was able to add aria-controls to ft-profile-selector because it keeps the hidden dropdown in the DOM. The same is not true of the ft-icon-button or ft-subscribe-button. Main point: aria-expanded should go on the button opening the dropdown, not the dropdown itself.

* Resolve subscribe button staying upon bug

* Remove unneeded vendor-specific transition prefixes

See discussion here: https://www.web-plus-plus.com/Articles/css-transition-moz-and-webkit-vs-css3
This commit is contained in:
Jason 2023-11-22 17:19:11 +00:00 committed by GitHub
parent 62e7b439db
commit e68c534e6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 308 additions and 68 deletions

View File

@ -11,17 +11,12 @@
overflow: hidden;
color: inherit;
text-decoration: none;
-webkit-transition: background 0.2s ease-out;
-moz-transition: background 0.2s ease-out;
-o-transition: background 0.2s ease-out;
transition: background 0.2s ease-out;
}
.bubblePadding:hover {
background-color: var(--side-nav-hover-color);
color: var(--side-nav-hover-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}

View File

@ -14,6 +14,7 @@
}"
tabindex="0"
role="button"
:aria-expanded="dropdownShown"
@click="handleIconClick"
@mousedown="handleIconMouseDown"
@keydown.enter.prevent="handleIconClick"
@ -33,7 +34,6 @@
v-if="dropdownOptions.length > 0"
class="list"
role="listbox"
:aria-expanded="dropdownShown"
>
<li
v-for="(option, index) in dropdownOptions"
@ -71,7 +71,6 @@
v-if="dropdownOptions.length > 0"
class="list"
role="listbox"
:aria-expanded="dropdownShown"
>
<li
v-for="(option, index) in dropdownOptions"

View File

@ -57,8 +57,6 @@
border-radius: 100%;
color: var(--primary-text-color);
opacity: 0;
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
@ -74,8 +72,6 @@
.clearInputTextButton.visible:active {
background-color: var(--tertiary-text-color);
color: var(--side-nav-active-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
@ -172,8 +168,6 @@
.inputAction.enabled:hover {
background-color: var(--side-nav-hover-color);
color: var(--side-nav-hover-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
@ -184,8 +178,6 @@
.inputAction.enabled:active {
background-color: var(--tertiary-text-color);
color: var(--side-nav-active-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}

View File

@ -4,17 +4,12 @@
padding-block: 10px 30px;
padding-inline: 10px;
cursor: pointer;
-webkit-transition: background 0.2s ease-out;
-moz-transition: background 0.2s ease-out;
-o-transition: background 0.2s ease-out;
transition: background 0.2s ease-out;
}
.bubblePadding:hover {
background-color: var(--side-nav-hover-color);
color: var(--side-nav-hover-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}

View File

@ -56,17 +56,12 @@ Height adjust for profile list so it won't cover navbar. */
.profile {
cursor: pointer;
block-size: 50px;
-webkit-transition: background 0.2s ease-out;
-moz-transition: background 0.2s ease-out;
-o-transition: background 0.2s ease-out;
transition: background 0.2s ease-out;
}
.profile:hover {
background-color: var(--side-nav-hover-color);
color: var(--side-nav-hover-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}

View File

@ -35,6 +35,10 @@ export default defineComponent({
}
},
methods: {
isActiveProfile: function (profile) {
return profile._id === this.activeProfile._id
},
toggleProfileList: function () {
this.profileListShown = !this.profileListShown

View File

@ -6,6 +6,8 @@
:style="{ background: activeProfile.bgColor, color: activeProfile.textColor }"
tabindex="0"
role="button"
:aria-expanded="profileListShown"
aria-controls="profileSelectorList"
@click="toggleProfileList"
@mousedown="handleIconMouseDown"
@keydown.space.prevent="toggleProfileList"
@ -19,6 +21,7 @@
</div>
<ft-card
v-show="profileListShown"
id="profileSelectorList"
ref="profileList"
class="profileList"
tabindex="-1"
@ -46,7 +49,7 @@
:key="index"
class="profile"
:aria-labelledby="'profile-' + index + '-name'"
aria-selected="false"
:aria-selected="isActiveProfile(profile)"
tabindex="0"
role="option"
@click="setActiveProfile(profile)"

View File

@ -1,8 +0,0 @@
/* Ensures style here overrides style of .btn */
.subscribeButton.btn {
align-self: center;
block-size: 50%;
margin-block-end: 10px;
min-inline-size: 150px;
white-space: initial;
}

View File

@ -25,29 +25,54 @@ export default defineComponent({
type: String,
required: true
},
hideProfileDropdownToggle: {
type: Boolean,
default: false
},
subscriptionCountText: {
default: '',
type: String,
required: false
}
},
data: function () {
return {
isProfileDropdownOpen: false
}
},
computed: {
profileInitials: function () {
return this.profileDisplayList.map((profile) => {
return profile?.name?.length > 0 ? Array.from(profile.name)[0].toUpperCase() : ''
})
},
profileList: function () {
return this.$store.getters.getProfileList
},
/* sort by 'All Channels' -> active profile -> unsubscribed channels -> subscribed channels */
profileDisplayList: function () {
const mainProfileAndActiveProfile = [this.profileList[0]]
if (this.activeProfile._id !== MAIN_PROFILE_ID) {
mainProfileAndActiveProfile.push(this.activeProfile)
}
return [
...mainProfileAndActiveProfile,
...this.profileList.filter((profile, i) =>
i !== 0 && !this.isActiveProfile(profile) && !this.isProfileSubscribed(profile)),
...this.profileList.filter((profile, i) =>
i !== 0 && !this.isActiveProfile(profile) && this.isProfileSubscribed(profile))
]
},
activeProfile: function () {
return this.$store.getters.getActiveProfile
},
subscriptionInfo: function () {
return this.activeProfile.subscriptions.find((channel) => {
return channel.id === this.channelId
}) ?? null
},
isSubscribed: function () {
return this.subscriptionInfo !== null
return this.subscriptionInfoForProfile(this.activeProfile)
},
hideChannelSubscriptions: function () {
@ -55,23 +80,27 @@ export default defineComponent({
},
subscribedText: function () {
let subscribedValue = (this.isSubscribed ? this.$t('Channel.Unsubscribe') : this.$t('Channel.Subscribe')).toUpperCase()
let subscribedValue = (this.isProfileSubscribed(this.activeProfile) ? this.$t('Channel.Unsubscribe') : this.$t('Channel.Subscribe')).toUpperCase()
if (this.subscriptionCountText !== '' && !this.hideChannelSubscriptions) {
subscribedValue += ' ' + this.subscriptionCountText
}
return subscribedValue
},
isProfileDropdownEnabled: function () {
return !this.hideProfileDropdownToggle && this.profileList.length > 1
}
},
methods: {
handleSubscription: function () {
handleSubscription: function (profile = this.activeProfile) {
if (this.channelId === '') {
return
}
const currentProfile = deepCopy(this.activeProfile)
const currentProfile = deepCopy(profile)
if (this.isSubscribed) {
if (this.isProfileSubscribed(profile)) {
currentProfile.subscriptions = currentProfile.subscriptions.filter((channel) => {
return channel.id !== this.channelId
})
@ -79,16 +108,16 @@ export default defineComponent({
this.updateProfile(currentProfile)
showToast(this.$t('Channel.Channel has been removed from your subscriptions'))
if (this.activeProfile._id === MAIN_PROFILE_ID) {
if (profile._id === MAIN_PROFILE_ID) {
// Check if a subscription exists in a different profile.
// Remove from there as well.
let duplicateSubscriptions = 0
this.profileList.forEach((profile) => {
if (profile._id === MAIN_PROFILE_ID) {
this.profileList.forEach((profileInList) => {
if (profileInList._id === MAIN_PROFILE_ID) {
return
}
duplicateSubscriptions += this.unsubscribe(profile, this.channelId)
duplicateSubscriptions += this.unsubscribe(profileInList, this.channelId)
})
if (duplicateSubscriptions > 0) {
@ -107,7 +136,7 @@ export default defineComponent({
this.updateProfile(currentProfile)
showToast(this.$t('Channel.Added channel to your subscriptions'))
if (this.activeProfile._id !== MAIN_PROFILE_ID) {
if (profile._id !== MAIN_PROFILE_ID) {
const primaryProfile = deepCopy(this.profileList.find(prof => {
return prof._id === MAIN_PROFILE_ID
}))
@ -122,6 +151,34 @@ export default defineComponent({
}
}
}
if (this.isProfileDropdownEnabled && !this.isProfileDropdownOpen) {
this.toggleProfileDropdown()
}
},
handleProfileDropdownFocusOut: function () {
if (!this.$refs.subscribeButton.matches(':focus-within')) {
this.isProfileDropdownOpen = false
}
},
toggleProfileDropdown: function() {
this.isProfileDropdownOpen = !this.isProfileDropdownOpen
},
isActiveProfile: function (profile) {
return profile._id === this.activeProfile._id
},
subscriptionInfoForProfile: function (profile) {
return profile.subscriptions.find((channel) => {
return channel.id === this.channelId
}) ?? null
},
isProfileSubscribed: function (profile) {
return this.subscriptionInfoForProfile(profile) !== null
},
unsubscribe: function(profile, channelId) {

View File

@ -0,0 +1,141 @@
.buttonList {
margin: 5px;
margin-block-end: 10px;
border-radius: 4px;
block-size: fit-content;
box-shadow: 0px 1px 2px rgb(0 0 0 / 50%);
display: flex;
flex-wrap: nowrap;
/* addresses odd clipping behavior when adjusting window size */
background-color: var(--primary-color);
}
.ftSubscribeButton {
position: relative;
text-align: start;
}
/* Ensures style here overrides style of .btn */
.subscribeButton.btn {
min-inline-size: 150px;
white-space: initial;
}
.subscribeButton.btn, .profileDropdownToggle.btn {
align-self: center;
margin-block: 0;
margin-inline: 0;
}
.dropdownOpened {
.subscribeButton, .profileDropdownToggle {
border-end-start-radius: 0;
border-end-end-radius: 0;
}
}
.profileDropdownToggle.btn {
border-inline-start: none !important;
border-start-start-radius: 0;
border-end-start-radius: 0;
display: inline-block;
min-inline-size: 1em;
padding-inline: 10px;
box-sizing: content-box;
}
.hasProfileDropdownToggle {
.subscribeButton.btn {
min-inline-size: 100px;
padding-inline: 5px;
border-inline-end: 2px solid var(--primary-color-active) !important;
border-start-end-radius: 0;
border-end-end-radius: 0;
box-sizing: content-box;
}
}
.hasProfileDropdownToggle > .subscribeButton.btn, .profileDropdownToggle.btn {
padding-block: 5px;
padding-inline: 6px;
box-shadow: none;
flex: auto;
block-size: 2em;
}
.profileDropdown {
background-color: var(--side-nav-color);
box-shadow: 0 1px 2px rgb(0 0 0 / 50%);
color: var(--secondary-text-color);
display: inline;
font-size: 12px;
max-block-size: 200px;
margin-block: -10px 0;
margin-inline: 5px 0;
overflow-y: scroll;
position: absolute;
text-align: center;
-webkit-user-select: none;
user-select: none;
z-index: 3;
// accounts for parent's left and right margins
inline-size: calc(100% - 10px);
.profileList {
list-style-type: none;
margin: 0;
padding-inline: 0;
}
.profile {
cursor: pointer;
display: flex;
gap: 0.5em;
padding-inline-start: 0.5em;
block-size: 50px;
align-items: center;
transition: background 0.2s ease-out;
&:hover {
background-color: var(--side-nav-hover-color);
color: var(--side-nav-hover-text-color);
transition: background 0.2s ease-in;
}
.colorOption {
inline-size: 40px;
block-size: 40px;
cursor: pointer;
align-items: center;
display: flex;
justify-content: center;
flex-shrink: 0;
border-radius: 50%;
-webkit-border-radius: 50%;
}
.initial {
font-size: 20px;
line-height: 1em;
text-align: center;
user-select: none;
-webkit-user-select: none;
}
.profileName {
padding-inline-end: 1em;
text-align: start;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&.subscribed {
background-color: var(--primary-color);
.profileName {
color: var(--text-with-main-color);
}
}
}
}

View File

@ -1,12 +1,83 @@
<template>
<ft-button
:label="subscribedText"
class="subscribeButton"
background-color="var(--primary-color)"
text-color="var(--text-with-main-color)"
@click="handleSubscription"
/>
<div
ref="subscribeButton"
class="ftSubscribeButton"
:class="{ dropdownOpened: isProfileDropdownOpen}"
@focusout="handleProfileDropdownFocusOut"
>
<div
class="buttonList"
:class="{ hasProfileDropdownToggle: isProfileDropdownEnabled}"
>
<ft-button
:label="subscribedText"
:no-border="true"
class="subscribeButton"
background-color="var(--primary-color)"
text-color="var(--text-with-main-color)"
@click="handleSubscription"
/>
<ft-button
v-if="isProfileDropdownEnabled"
:no-border="true"
:title="isProfileDropdownOpen ? $t('Profile.Close Profile Dropdown') : $t('Profile.Open Profile Dropdown')"
class="profileDropdownToggle"
background-color="var(--primary-color)"
text-color="var(--text-with-main-color)"
:aria-expanded="isProfileDropdownOpen"
@click="toggleProfileDropdown"
>
<font-awesome-icon
:icon="isProfileDropdownOpen ? ['fas', 'angle-up'] : ['fas', 'angle-down']"
/>
</ft-button>
</div>
<template v-if="isProfileDropdownOpen">
<div
tabindex="-1"
class="profileDropdown"
>
<ul
class="profileList"
>
<li
v-for="(profile, index) in profileDisplayList"
:id="'subscription-profile-' + index"
:key="index"
class="profile"
:class="{
subscribed: isProfileSubscribed(profile)
}"
:aria-labelledby="'subscription-profile-' + index + '-name'"
:aria-selected="isActiveProfile(profile)"
:aria-checked="isProfileSubscribed(profile)"
tabindex="0"
role="checkbox"
@click.stop.prevent="handleSubscription(profile)"
@keydown.space.stop.prevent="handleSubscription(profile)"
>
<div
class="colorOption"
:style="{ background: profile.bgColor, color: profile.textColor }"
>
<div
class="initial"
>
{{ isProfileSubscribed(profile) ? '✓' : profileInitials[index] }}
</div>
</div>
<p
:id="'subscription-profile-' + index + '-name'"
class="profileName"
>
{{ profile.name }}
</p>
</li>
</ul>
</div>
</template>
</div>
</template>
<script src="./ft-subscribe-button.js" />
<style src="./ft-subscribe-button.css" />
<style lang="scss" src="./ft-subscribe-button.scss" />

View File

@ -14,16 +14,12 @@
.navOption:hover {
background-color: var(--side-nav-hover-color);
color: var(--side-nav-hover-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.navOption:active {
background-color: var(--side-nav-active-color);
color: var(--side-nav-active-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}

View File

@ -53,16 +53,12 @@
.navOption:hover, .navChannel:hover {
background-color: var(--side-nav-hover-color);
color: var(--side-nav-hover-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}
.navOption:active, .navChannel:active {
background-color: var(--side-nav-active-color);
color: var(--side-nav-active-text-color);
-moz-transition: background 0.2s ease-in;
-o-transition: background 0.2s ease-in;
transition: background 0.2s ease-in;
}

View File

@ -9,6 +9,7 @@ import { library } from '@fortawesome/fontawesome-svg-core'
// to avoid code conflict and duplicate entries
import {
faAngleDown,
faAngleUp,
faArrowDown,
faArrowLeft,
faArrowRight,
@ -81,6 +82,7 @@ Vue.config.productionTip = process.env.NODE_ENV === 'development'
library.add(
// solid icons
faAngleDown,
faAngleUp,
faArrowDown,
faArrowLeft,
faArrowRight,

View File

@ -69,6 +69,7 @@
display: flex;
gap: 30px;
justify-content: space-between;
align-items: center;
}
.subscribeButton {

View File

@ -26,7 +26,7 @@
<ft-flex-box class="channels">
<div
v-for="channel in channelList"
:key="channel.key"
:key="channel.id"
class="channel"
>
<router-link
@ -53,7 +53,6 @@
class="unsubscribeContainer"
>
<ft-subscribe-button
class="btn"
:channel-id="channel.id"
:channel-name="channel.name"
:channel-thumbnail="channel.thumbnail"

View File

@ -551,6 +551,8 @@ Profile:
Are you sure you want to delete the selected channels? This will not delete the channel from any other profile.: Are
you sure you want to delete the selected channels? This will not delete the channel
from any other profile.
Close Profile Dropdown: Close Profile Dropdown
Open Profile Dropdown: Open Profile Dropdown
#On Channel Page
Channel:
Subscribe: Subscribe