Merge pull request #22222 from overleaf/td-thin-footer-react

Implement thin footer in Bootstrap 5

GitOrigin-RevId: c45f63b3d2536e6266eb55cc95df11b53a6b77ec
This commit is contained in:
Tim Down
2024-12-16 11:39:46 +00:00
committed by Copybot
parent 0c40841d9f
commit 79e4b76551
33 changed files with 294 additions and 147 deletions
+1 -2
View File
@@ -83,8 +83,7 @@ describe('LearnWiki', function () {
})
function checkDisabled() {
// eslint-disable-next-line mocha/no-skipped-tests
it.skip('should not add a documentation entry to the nav bar', () => {
it('should not add a documentation entry to the nav bar', () => {
login(REGULAR_USER)
cy.visit('/project')
cy.findByText('Documentation').should('not.exist')
@@ -244,8 +244,8 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) {
}
// This function is used to add translations from the server for main
// navigation items because it's tricky to get them in the front end
// otherwise.
// navigation and footer items because it's tricky to get them in the front
// end otherwise.
res.locals.cloneAndTranslateText = obj => {
const clone = _.cloneDeep(obj)
addTranslatedTextDeep(clone)
+4 -1
View File
@@ -17,7 +17,10 @@ block body
if (typeof suppressFooter === "undefined")
if showThinFooter
include layout/footer-marketing
if bootstrapVersion === 5
include layout/thin-footer-bootstrap-5
else
include layout/thin-footer
else
include layout/fat-footer
+9 -2
View File
@@ -40,8 +40,12 @@ block append meta
items: cloneAndTranslateText(nav.header_extras)
})
meta(name="ol-footer" data-type="json" content={
showThinFooter: showThinFooter,
showPoweredBy: !hasFeature('saas') && !settings.nav.hide_powered_by,
subdomainLang: settings.i18n.subdomainLang,
translatedLanguages: settings.translatedLanguages
translatedLanguages: settings.translatedLanguages,
leftItems: cloneAndTranslateText(settings.nav.left_footer),
rightItems: settings.nav.right_footer
})
block body
@@ -55,7 +59,10 @@ block body
if (typeof suppressFooter === "undefined")
if showThinFooter
include layout/footer-marketing
if bootstrapVersion === 5
include layout/thin-footer-bootstrap-5
else
include layout/thin-footer
else
if bootstrapVersion === 5
include layout/fat-footer-react-bootstrap-5
@@ -16,7 +16,7 @@ block body
if (typeof(suppressFooter) == "undefined")
if showThinFooter
include layout/footer-marketing
include layout/thin-footer-bootstrap-5
else
include layout/fat-footer-website-redesign
@@ -14,7 +14,7 @@ block body
if (typeof(suppressFooter) == "undefined")
if showThinFooter
include layout/footer-marketing
include layout/thin-footer
else
include layout/fat-footer-website-redesign
@@ -1 +1 @@
#fat-footer-container
#footer-container
@@ -1,21 +1,19 @@
li.dropdown.dropup.subdued(dropdown).language-picker
a#language-picker-toggle(
href="#",
button#language-picker-toggle.btn.btn-link.btn-inline-link(
dropdown-toggle,
data-ol-lang-selector-tooltip,
data-bs-toggle="dropdown",
role="button"
aria-haspopup="true",
aria-expanded="false",
aria-label="Select " + translate('language'),
tooltip=translate('language')
title=translate('language')
)
i.fa.fa-fw.fa-language
|
| #{settings.translatedLanguages[currentLngCode]}
span.material-symbols(aria-hidden="true") translate
|  
span.language-picker-text #{settings.translatedLanguages[currentLngCode]}
ul.dropdown-menu(role="menu" aria-labelledby="language-picker-toggle")
ul.dropdown-menu.dropdown-menu-sm-width(role="menu" aria-labelledby="language-picker-toggle")
li.dropdown-header #{translate("language")}
each subdomainDetails, subdomain in settings.i18n.subdomainLang
if !subdomainDetails.hide
@@ -24,4 +22,4 @@ li.dropdown.dropup.subdued(dropdown).language-picker
a.menu-indent(href=subdomainDetails.url+currentUrlWithQueryParams, role="menuitem", class=isActive ? 'dropdown-item active' : 'dropdown-item', aria-selected=isActive ? 'true' : 'false')
| #{settings.translatedLanguages[subdomainDetails.lngCode]}
if subdomainDetails.lngCode === currentLngCode
span.material-symbols.dropdown-item-trailing-icon.pull-right(aria-hidden="true") check
span.material-symbols.dropdown-item-trailing-icon(aria-hidden="true") check
@@ -0,0 +1,38 @@
footer.site-footer
- var showLanguagePicker = Object.keys(settings.i18n.subdomainLang).length > 1
- var hasCustomLeftNav = nav.left_footer && nav.left_footer.length > 0
.site-footer-content.hidden-print
.row
ul.site-footer-items.col-lg-9
if !settings.nav.hide_powered_by
li
//- year of Server Pro release, static
| © 2024
|
a(href='https://www.overleaf.com/for/enterprises') Powered by Overleaf
if showLanguagePicker || hasCustomLeftNav
li
strong.text-muted |
if showLanguagePicker
include language-picker-bootstrap-5
if showLanguagePicker && hasCustomLeftNav
li
strong.text-muted |
each item in nav.left_footer
li
if item.url
a(href=item.url, class=item.class) !{translate(item.text)}
else
| !{item.text}
ul.site-footer-items.col-lg-3.text-end
each item in nav.right_footer
li
if item.url
a(href=item.url, class=item.class, aria-label=item.label) !{item.text}
else
| !{item.text}
@@ -790,6 +790,7 @@
"knowledge_base": "",
"labels_help_you_to_easily_reference_your_figures": "",
"labels_help_you_to_reference_your_tables": "",
"language": "",
"language_feedback": "",
"large_or_high-resolution_images_taking_too_long": "",
"last_active": "",
@@ -1,7 +1,7 @@
import ReactDOM from 'react-dom'
import getMeta from '@/utils/meta'
import DefaultNavbar from '@/features/ui/components/bootstrap-5/navbar/default-navbar'
import FatFooter from '@/features/ui/components/bootstrap-5/footer/fat-footer'
import Footer from '@/features/ui/components/bootstrap-5/footer/footer'
const navbarElement = document.getElementById('navbar-container')
if (navbarElement) {
@@ -9,8 +9,8 @@ if (navbarElement) {
ReactDOM.render(<DefaultNavbar {...navbarProps} />, navbarElement)
}
const footerElement = document.getElementById('fat-footer-container')
const footerElement = document.getElementById('footer-container')
if (footerElement) {
const footerProps = getMeta('ol-footer')
ReactDOM.render(<FatFooter {...footerProps} />, footerElement)
ReactDOM.render(<Footer {...footerProps} />, footerElement)
}
@@ -22,7 +22,6 @@ import SidebarDsNav from '@/features/project-list/components/sidebar/sidebar-ds-
export function ProjectListDsNav() {
const navbarProps = getMeta('ol-navbar')
const footerProps = getMeta('ol-footer')
const { t } = useTranslation()
const {
error,
@@ -120,7 +119,7 @@ export function ProjectListDsNav() {
</div>
</div>
</div>
<FatFooter {...footerProps} />
<FatFooter />
</div>
</main>
</div>
@@ -17,7 +17,7 @@ import withErrorBoundary from '../../../infrastructure/error-boundary'
import { GenericErrorBoundaryFallback } from '@/shared/components/generic-error-boundary-fallback'
import getMeta from '@/utils/meta'
import DefaultNavbar from '@/features/ui/components/bootstrap-5/navbar/default-navbar'
import FatFooter from '@/features/ui/components/bootstrap-5/footer/fat-footer'
import Footer from '@/features/ui/components/bootstrap-5/footer/footer'
import WelcomePageContent from '@/features/project-list/components/welcome-page-content'
import ProjectListDefault from '@/features/project-list/components/project-list-default'
import { ProjectListDsNav } from '@/features/project-list/components/project-list-ds-nav'
@@ -57,7 +57,7 @@ function DefaultNavbarAndFooter({ children }: { children: ReactNode }) {
>
{children}
</main>
<FatFooter {...footerProps} />
<Footer {...footerProps} />
</>
)
}
@@ -33,7 +33,7 @@ function FatFooterBase() {
</FooterBaseLink>
</div>
<div className="fat-footer-base-item fat-footer-base-language">
<LanguagePicker />
<LanguagePicker showHeader={false} />
</div>
</div>
<div className="fat-footer-base-section fat-footer-base-social">
@@ -1,7 +1,6 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import FatFooterBase from './fat-footer-base'
import { FatFooterMetadata } from '../../types/fat-footer-metadata'
type FooterLinkProps = {
href: string
@@ -13,7 +12,7 @@ type FooterSectionProps = {
links: FooterLinkProps[]
}
function FatFooter(props: FatFooterMetadata) {
function FatFooter() {
const { t } = useTranslation()
const hideFatFooter = false
@@ -0,0 +1,9 @@
import { FooterMetadata } from '@/features/ui/components/types/footer-metadata'
import ThinFooter from '@/features/ui/components/bootstrap-5/footer/thin-footer'
import FatFooter from '@/features/ui/components/bootstrap-5/footer/fat-footer'
function Footer(props: FooterMetadata) {
return props.showThinFooter ? <ThinFooter {...props} /> : <FatFooter />
}
export default Footer
@@ -0,0 +1,90 @@
import type {
FooterItem,
FooterMetadata,
} from '@/features/ui/components/types/footer-metadata'
import OLRow from '@/features/ui/components/ol/ol-row'
import LanguagePicker from '@/features/ui/components/bootstrap-5/language-picker'
import React from 'react'
function FooterItemLi({
text,
translatedText,
url: href,
class: className,
label,
}: FooterItem) {
const textToDisplay = translatedText || text
if (!href) {
return <li>{textToDisplay}</li>
}
const linkProps = {
href,
className,
'aria-label': label,
}
return (
<li>
<a {...linkProps} dangerouslySetInnerHTML={{ __html: textToDisplay }} />
</li>
)
}
function Separator() {
return (
<li role="separator" className="text-muted">
<strong>|</strong>
</li>
)
}
function ThinFooter({
showPoweredBy,
subdomainLang,
leftItems,
rightItems,
}: FooterMetadata) {
const showLanguagePicker = Boolean(
subdomainLang && Object.keys(subdomainLang).length > 1
)
const hasCustomLeftNav = Boolean(leftItems && leftItems.length > 0)
return (
<footer className="site-footer">
<div className="site-footer-content d-print-none">
<OLRow>
<ul className="site-footer-items col-lg-9">
{showPoweredBy ? (
<>
<li>
{/* year of Server Pro release, static */}© 2024{' '}
<a href="https://www.overleaf.com/for/enterprises">
Powered by Overleaf
</a>
</li>
{showLanguagePicker || hasCustomLeftNav ? <Separator /> : null}
</>
) : null}
{showLanguagePicker ? (
<li>
<LanguagePicker showHeader />
</li>
) : null}
{hasCustomLeftNav ? <Separator /> : null}
{leftItems?.map(item => <FooterItemLi key={item.text} {...item} />)}
</ul>
<ul className="site-footer-items col-lg-3 text-end">
{rightItems?.map(item => (
<FooterItemLi key={item.text} {...item} />
))}
</ul>
</OLRow>
</div>
</footer>
)
}
export default ThinFooter
@@ -4,12 +4,13 @@ import {
DropdownItem,
DropdownMenu,
DropdownToggle,
DropdownHeader,
} from './dropdown-menu'
import { useTranslation } from 'react-i18next'
import getMeta from '@/utils/meta'
import Icon from '@/shared/components/icon'
import MaterialIcon from '@/shared/components/material-icon'
function LanguagePicker() {
function LanguagePicker({ showHeader } = { showHeader: false }) {
const { t } = useTranslation()
const currentLangCode = getMeta('ol-i18n').currentLangCode
@@ -26,18 +27,18 @@ function LanguagePicker() {
className="btn-inline-link"
variant="link"
>
<Icon
type="language"
className="fa fa-fw"
accessibilityLabel={t('select_a_language')}
/>
{translatedLanguages?.[currentLangCode]}
<MaterialIcon type="translate" />
&nbsp;
<span className="language-picker-text">
{translatedLanguages?.[currentLangCode]}
</span>
</DropdownToggle>
<DropdownMenu
className="dropdown-menu-sm-width"
aria-labelledby="language-picker-toggle"
>
{showHeader ? <DropdownHeader>{t('language')}</DropdownHeader> : null}
{subdomainLang &&
Object.entries(subdomainLang).map(([subdomain, subdomainDetails]) => {
if (
@@ -1,7 +0,0 @@
import type { SubdomainLang } from '@/features/ui/components/types/fat-footer'
export type FatFooterMetadata = {
subdomainLang?: SubdomainLang
translatedLanguages: { [key: string]: string }
currentLangCode: string
}
@@ -0,0 +1,18 @@
import type { SubdomainLang } from '@/features/ui/components/types/fat-footer'
export type FooterItem = {
text: string
translatedText?: string
url?: string
class?: string
label?: string
}
export type FooterMetadata = {
showThinFooter: boolean
translatedLanguages: { [key: string]: string }
showPoweredBy?: boolean
subdomainLang?: SubdomainLang
leftItems?: FooterItem[]
rightItems?: FooterItem[]
}
+2 -2
View File
@@ -49,7 +49,7 @@ import { ThirdPartyIds } from '../../../types/third-party-ids'
import { Publisher } from '../../../types/subscription/dashboard/publisher'
import { SubscriptionChangePreview } from '../../../types/subscription/subscription-change-preview'
import { DefaultNavbarMetadata } from '@/features/ui/components/types/default-navbar-metadata'
import { FatFooterMetadata } from '@/features/ui/components/types/fat-footer-metadata'
import { FooterMetadata } from '@/features/ui/components/types/footer-metadata'
export interface Meta {
'ol-ExposedSettings': ExposedSettings
'ol-allInReconfirmNotificationPeriods': UserEmailData[]
@@ -89,7 +89,7 @@ export interface Meta {
'ol-error': { name: string } | undefined
'ol-expired': boolean
'ol-features': Features
'ol-footer': FatFooterMetadata
'ol-footer': FooterMetadata
'ol-fromPlansPage': boolean
'ol-galleryTagName': string
'ol-gitBridgeEnabled': boolean
@@ -1,10 +1,14 @@
// Footer
$footer-height: 50px;
// Header
$header-height: 68px;
$toolbar-height: 40px;
$toolbar-small-height: 32px;
// Footer
$thin-footer-height: 50px;
$fat-footer-content-height: calc(100vh - $header-height);
$thin-footer-content-height: calc(
100vh - ($header-height + $thin-footer-height)
);
// List of allowed themes
$themes: ('default', 'light');
@@ -1,7 +1,15 @@
body {
position: relative;
}
.content {
min-height: 100vh;
padding-top: $header-height + $spacing-08;
padding-bottom: $spacing-08;
.thin-footer & {
min-height: calc(100vh - #{$thin-footer-height});
}
}
.content-alt {
@@ -8,11 +8,13 @@
--link-color-dark: var(--link-ui-dark);
--link-hover-color-dark: var(--link-ui-hover-dark);
--link-visited-color-dark: var(--link-ui-visited-dark);
--link-text-decoration: underline;
--link-hover-text-decoration: none;
}
a {
color: var(--link-color);
text-decoration: var(--link-text-decoration);
&:visited {
color: var(--link-visited-color);
@@ -137,7 +137,7 @@
// Make a button look and behave like a link
.btn-link {
color: var(--link-ui);
color: var(--link-color);
font-weight: normal;
cursor: pointer;
text-decoration: underline;
@@ -156,7 +156,7 @@
&:hover,
&:focus {
color: var(--link-ui-hover);
color: var(--link-hover-color);
text-decoration: none;
background-color: transparent;
}
@@ -1,3 +1,5 @@
$dropdown-item-min-height: 36px;
.dropdown {
display: inline-flex;
@@ -20,6 +22,9 @@
.dropdown-header {
@include body-xs;
display: flex;
align-items: center;
min-height: $dropdown-item-min-height; // a minimum height of 36px to be accessible for touch screens
padding: var(--spacing-05) var(--spacing-06) var(--spacing-02)
var(--spacing-04);
}
@@ -55,7 +60,7 @@
display: grid;
grid-auto-flow: column;
place-content: center start;
min-height: 36px; // a minimum height of 36px to be accessible for touch screens
min-height: $dropdown-item-min-height; // a minimum height of 36px to be accessible for touch screens
position: relative;
&:active {
@@ -56,56 +56,33 @@ $footer-width-delta: 320px;
}
footer.site-footer {
--link-color: var(--link-web);
--link-hover-color: var(--link-web-hover);
--link-visited-color: var(--link-web-visited);
--link-text-decoration: none;
--link-hover-text-decoration: underline;
background-color: var(--bg-light-primary);
border-top: 1px solid var(--border-primary-dark);
position: absolute;
bottom: 0;
width: 100%;
@include body-sm;
height: $footer-height;
min-height: $thin-footer-height;
line-height: $thin-footer-height - 1; // Hack — in Chrome, using the full $footer-height would generate vertical scrolling
ul {
ul.site-footer-items {
list-style: none;
margin: var(--spacing-00);
margin: 0;
li {
> li {
display: inline-block;
margin: var(--spacing-07) var(--spacing-04);
margin: 0 var(--spacing-04);
}
i {
font-size: var(--font-size-05);
}
}
li.lng-option {
text-align: left;
display: list-item;
img {
vertical-align: text-bottom;
}
a:hover,
a:focus {
color: var(--content-primary-dark);
&:visited {
color: var(--content-primary-dark);
}
}
}
a {
color: var(--content-secondary-dark);
&:hover,
&:focus {
color: var(--content-secondary-dark);
}
}
}
.site-footer-content {
@@ -117,7 +94,44 @@ footer.site-footer {
vertical-align: middle;
}
#language-picker-toggle {
text-decoration: none;
cursor: pointer;
&::after {
display: none;
}
&:hover {
.language-picker-text {
text-decoration: var(--link-hover-text-decoration);
}
}
.material-symbols {
vertical-align: -0.1875rem;
}
}
.language-picker .dropdown-menu {
.dropdown-item {
&.active {
color: var(--green-70);
}
&:hover {
text-decoration: none;
}
}
}
.fat-footer {
--link-color: var(--content-secondary-dark);
--link-hover-color: var(--content-secondary-dark);
--link-visited-color: var(--content-secondary-dark);
--link-text-decoration: none;
--link-hover-text-decoration: underline;
container-name: fatfooter;
container-type: inline-size;
background: var(--bg-dark-primary);
@@ -133,20 +147,6 @@ footer.site-footer {
}
}
a {
color: var(--content-secondary-dark);
text-decoration: none;
&:hover {
color: var(--content-secondary-dark);
text-decoration: underline;
}
&:visited {
color: var(--content-secondary-dark);
}
}
.footer-brand-container {
flex: 1;
}
@@ -196,16 +196,6 @@ footer.site-footer {
#language-picker-toggle {
color: var(--content-secondary-dark);
cursor: pointer;
text-decoration: none;
&::after {
display: none;
}
&:hover {
text-decoration: underline;
}
}
.fat-footer-base-meta a:not(.dropdown-toggle) {
@@ -216,16 +206,6 @@ footer.site-footer {
.dropdown-header {
display: none; /* hiding rather than removing as still needed in the thin footer */
}
.dropdown-item {
&.active {
color: var(--green-70);
}
&:hover {
text-decoration: none;
}
}
}
}
@@ -406,17 +386,13 @@ footer.site-footer {
}
&.fat-footer {
--link-color: var(--content-primary);
--link-hover-color: var(--content-primary);
--link-visited-color: var(--content-primary);
background: var(--content-primary-dark);
color: var(--content-primary);
a {
color: var(--content-primary);
&:visited {
color: var(--content-primary);
}
}
.footer-section-heading {
color: var(--content-primary);
}
@@ -1,24 +1,30 @@
.project-list-react {
--project-list-content-height: #{$fat-footer-content-height};
#project-list-root > &.content {
padding-top: $header-height;
padding-bottom: 0;
min-height: calc(100vh - #{$header-height});
min-height: var(--project-list-content-height);
display: flex;
flex-direction: column;
.thin-footer & {
--project-list-content-height: #{$thin-footer-content-height};
}
}
.project-list-wrapper {
display: flex;
align-items: stretch;
width: 100%;
min-height: calc(100vh - #{$header-height});
min-height: var(--project-list-content-height);
}
.project-list-sidebar-wrapper-react {
position: relative;
background-color: var(--bg-dark-secondary);
flex: 0 0 15%;
min-height: calc(100vh - #{$header-height});
min-height: var(--project-list-content-height);
max-width: 320px;
min-width: 200px;
@@ -121,7 +127,7 @@
flex-direction: column;
justify-content: center;
align-items: center;
min-height: calc(100vh - #{$header-height});
min-height: var(--project-list-content-height);
.loading-screen-brand-container {
margin: 0 auto;
@@ -579,18 +579,6 @@
}
}
}
.loading-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: calc(100vh - #{$header-height});
.loading-screen-brand-container {
margin: 0 auto;
}
}
}
.current-plan {
@@ -10,6 +10,7 @@ describe('LanguagePicker', function () {
currentLangCode: 'en',
})
window.metaAttributesCache.set('ol-footer', {
showThinFooter: false,
translatedLanguages: {
en: 'English',
fr: 'Français',
@@ -26,12 +27,12 @@ describe('LanguagePicker', function () {
})
it('renders the language picker with the current language', function () {
cy.mount(<LanguagePicker />)
cy.mount(<LanguagePicker showHeader />)
cy.get('#language-picker-toggle').should('contain', 'English')
})
it('opens the dropdown and lists available languages', function () {
cy.mount(<LanguagePicker />)
cy.mount(<LanguagePicker showHeader />)
cy.get('#language-picker-toggle').click()
cy.get('.dropdown-menu').within(() => {
@@ -42,7 +43,7 @@ describe('LanguagePicker', function () {
})
it('changes the language and updates the URL when a language is selected', function () {
cy.mount(<LanguagePicker />)
cy.mount(<LanguagePicker showHeader />)
cy.get('#language-picker-toggle').should('exist').click()
cy.contains('Français').click()
cy.url().should('include', 'fr.overleaf.com')
@@ -55,6 +55,7 @@ describe('<ProjectListRoot />', function () {
window.metaAttributesCache.set('ol-user', {})
window.metaAttributesCache.set('ol-user_id', userId)
window.metaAttributesCache.set('ol-footer', {
showThinFooter: false,
translatedLanguages: { en: 'English' },
subdomainLang: { en: { lngCode: 'en', url: 'overleaf.com' } },
})
@@ -65,6 +65,7 @@ describe('<ProjectTools />', function () {
})
window.metaAttributesCache.set('ol-footer', {
showThinFooter: false,
translatedLanguages: { en: 'English' },
subdomainLang: { en: { lngCode: 'en', url: 'overleaf.com' } },
})