diff --git a/src/fonts.ts b/src/fonts.ts new file mode 100644 index 000000000..3be23a85a --- /dev/null +++ b/src/fonts.ts @@ -0,0 +1,68 @@ +/** + * Font information for solid-ui / Mashlib. + * + * The UI references the following font families in its inline styles: + * + * - **Raleway** and **Roboto**: used for primary and secondary buttons + * (`font-family: Raleway, Roboto, sans-serif`). These are web fonts that + * are _not_ bundled with the library; they must be loaded separately by the + * host application (e.g. via `loadFonts()`). + * + * - **Arial**: used for header menu links and buttons. Arial is a system font + * on Windows and macOS; on other platforms the browser will fall back to the + * next `sans-serif` font. + * + * - **monospace** and **sans-serif**: generic CSS font families used for the + * notepad / pad widget and heading elements respectively. + * + * Use `loadFonts()` to inject the Raleway and Roboto web fonts into the + * document so that they are available on all platforms. + */ + +import styleConstants from './styleConstants' + +/** Names of the web fonts that solid-ui references in its styles. */ +export const fontNames = { + /** Primary font for buttons. */ + button: 'Raleway', + /** Fallback font for buttons when Raleway is unavailable. */ + buttonFallback: 'Roboto' +} + +/** + * Full CSS font-family stacks as used in the inline styles. + * These match the values in `styleConstants`. + */ +export const fontFamilies = { + /** Font stack for primary and secondary buttons. */ + button: styleConstants.fontFamilyButton, + /** Font stack for header menus and navigation links. */ + header: styleConstants.fontFamilyHeader +} + +/** + * Injects a `` element into the document `` to load Raleway and + * Roboto from the provided base URL (defaults to Google Fonts). + * + * Call this once during application initialisation so that the web fonts + * referenced in solid-ui styles are actually available to the browser. + * + * @param fontsUrl - The URL of the CSS file that declares `@font-face` rules + * for Raleway and Roboto. Defaults to a Google Fonts stylesheet. Pass a + * self-hosted URL to avoid sending requests to third-party servers. + */ +export function loadFonts ( + fontsUrl = 'https://fonts.googleapis.com/css2?family=Raleway:wght@400;700&family=Roboto:wght@400;700&display=swap' +): void { + if (typeof document === 'undefined') return + + // Avoid adding duplicate elements. + const existing = document.querySelector(`link[data-solid-ui-fonts]`) + if (existing) return + + const link = document.createElement('link') + link.rel = 'stylesheet' + link.href = fontsUrl + link.setAttribute('data-solid-ui-fonts', 'true') + document.head.appendChild(link) +} diff --git a/src/index.ts b/src/index.ts index 2c8f1bcbd..a1bce066e 100755 --- a/src/index.ts +++ b/src/index.ts @@ -57,6 +57,7 @@ import * as pad from './pad' import * as participation from './participation' // @ts-ignore import * as preferences from './preferences' +import * as fonts from './fonts' // @ts-ignore import { style } from './style' // @ts-ignore @@ -80,6 +81,7 @@ if (typeof window !== 'undefined') { create, createTypes, dom, + fonts, icons, language, log, @@ -109,6 +111,7 @@ export { create, createTypes, dom, + fonts, icons, language, log, diff --git a/src/style.js b/src/style.js index 187160c85..f6605f0fd 100644 --- a/src/style.js +++ b/src/style.js @@ -89,12 +89,12 @@ export const style = { // styleModule temporaryStatusEnd: 'background: transparent; transition: background 5s linear;', // header - headerUserMenuLink: 'background: none; border: 0; color: black; cursor: pointer; display: block; font-family: Arial; font-size: 1em; text-align: left; padding: 1em; width: 100%; text-decoration: none;', - headerUserMenuLinkHover: 'background: none; border: 0; color: black; cursor: pointer; display: block; font-family: Arial; font-size: 1em; text-align: left; padding: 1em; width: 100%; text-decoration: none; background-image: linear-gradient(to right, #7C4DFF 0%, #18A9E6 50%, #01C9EA 100%);', + headerUserMenuLink: `background: none; border: 0; color: black; cursor: pointer; display: block; font-family: ${styleConstants.fontFamilyHeader}; font-size: 1em; text-align: left; padding: 1em; width: 100%; text-decoration: none;`, + headerUserMenuLinkHover: `background: none; border: 0; color: black; cursor: pointer; display: block; font-family: ${styleConstants.fontFamilyHeader}; font-size: 1em; text-align: left; padding: 1em; width: 100%; text-decoration: none; background-image: linear-gradient(to right, #7C4DFF 0%, #18A9E6 50%, #01C9EA 100%);`, headerUserMenuTrigger: 'background: none; border: 0; cursor: pointer; width: 60px; height: 60px;', headerUserMenuTriggerImg: 'border-radius: 50%; height: 56px; width: 28px !important;', - headerUserMenuButton: 'background: none; border: 0; color: black; cursor: pointer; display: block; font-family: Arial; font-size: 1em; text-align: left; padding: 1em; width: 100%;', - headerUserMenuButtonHover: 'background: none; border: 0; color: black; cursor: pointer; display: block; font-family: Arial; font-size: 1em; text-align: left; padding: 1em; width: 100%; background-image: linear-gradient(to right, #7C4DFF 0%, #18A9E6 50%, #01C9EA 100%);', + headerUserMenuButton: `background: none; border: 0; color: black; cursor: pointer; display: block; font-family: ${styleConstants.fontFamilyHeader}; font-size: 1em; text-align: left; padding: 1em; width: 100%;`, + headerUserMenuButtonHover: `background: none; border: 0; color: black; cursor: pointer; display: block; font-family: ${styleConstants.fontFamilyHeader}; font-size: 1em; text-align: left; padding: 1em; width: 100%; background-image: linear-gradient(to right, #7C4DFF 0%, #18A9E6 50%, #01C9EA 100%);`, headerUserMenuList: 'list-style: none; margin: 0; padding: 0;', headerUserMenuListDisplay: 'list-style: none; margin: 0; padding: 0; display:true;', headerUserMenuNavigationMenu: 'background: white; border: solid 1px #000000; border-right: 0; position: absolute; right: 0; top: 60px; width: 200px; z-index: 1; display: true;', @@ -114,14 +114,14 @@ export const style = { // styleModule footer: 'border-top: solid 1px $divider-color; font-size: 0.9em; padding: 0.5em 1.5em;', // buttons - primaryButton: 'background-color: #7c4dff; color: #ffffff; font-family: Raleway, Roboto, sans-serif; border-radius: 0.25em; border-color: #7c4dff; border: 1px solid; cursor: pointer; font-size: .8em; text-decoration: none; padding: 0.5em 4em; transition: 0.25s all ease-in-out; outline: none;', - primaryButtonHover: 'background-color: #9f7dff; color: #ffffff; font-family: Raleway, Roboto, sans-serif;border-radius: 0.25em; border-color: #7c4dff; border: 1px solid; cursor: pointer; font-size: .8em; text-decoration: none; padding: 0.5em 4em; transition: 0.25s all ease-in-out; outline: none; transition: 0.25s all ease-in-out;', - primaryButtonNoBorder: 'background-color: #ffffff; color: #7c4dff; font-family: Raleway, Roboto, sans-serif;border-radius: 0.25em; border-color: #7c4dff; border: 1px solid; cursor: pointer; font-size: .8em; text-decoration: none; padding: 0.5em 4em; transition: 0.25s all ease-in-out; outline: none;', - primaryButtonNoBorderHover: 'background-color: #7c4dff; color: #ffffff; font-family: Raleway, Roboto, sans-serif; border-radius: 0.25em; border-color: #7c4dff; border: 1px solid; cursor: pointer; font-size: .8em; text-decoration: none; padding: 0.5em 4em; transition: 0.25s all ease-in-out; outline: none; transition: 0.25s all ease-in-out;', - secondaryButton: 'background-color: #01c9ea; color: #ffffff; font-family: Raleway, Roboto, sans-serif;border-radius: 0.25em; border-color: #01c9ea; border: 1px solid; cursor: pointer; font-size: .8em; text-decoration: none; padding: 0.5em 4em; transition: 0.25s all ease-in-out; outline: none;', - secondaryButtonHover: 'background-color: #37cde6; color: #ffffff; font-family: Raleway, Roboto, sans-serif;border-radius: 0.25em; border-color: #7c4dff; border: 1px solid; cursor: pointer; font-size: .8em; text-decoration: none; padding: 0.5em 4em; transition: 0.25s all ease-in-out; outline: none; transition: 0.25s all ease-in-out;', - secondaryButtonNoBorder: 'background-color: #ffffff; color: #01c9ea; font-family: Raleway, Roboto, sans-serif; border-radius: 0.25em; border-color: #01c9ea; border: 1px solid; cursor: pointer; font-size: .8em; text-decoration: none; padding: 0.5em 4em; transition: 0.25s all ease-in-out; outline: none;', - secondaryButtonNoBorderHover: 'background-color: #01c9ea; color: #ffffff; font-family: Raleway, Roboto, sans-serif; border-radius: 0.25em; border-color: #01c9ea; border: 1px solid; cursor: pointer; font-size: .8em; text-decoration: none; padding: 0.5em 4em; transition: 0.25s all ease-in-out; outline: none; transition: 0.25s all ease-in-out;', + primaryButton: `background-color: #7c4dff; color: #ffffff; font-family: ${styleConstants.fontFamilyButton}; border-radius: 0.25em; border-color: #7c4dff; border: 1px solid; cursor: pointer; font-size: .8em; text-decoration: none; padding: 0.5em 4em; transition: 0.25s all ease-in-out; outline: none;`, + primaryButtonHover: `background-color: #9f7dff; color: #ffffff; font-family: ${styleConstants.fontFamilyButton}; border-radius: 0.25em; border-color: #7c4dff; border: 1px solid; cursor: pointer; font-size: .8em; text-decoration: none; padding: 0.5em 4em; transition: 0.25s all ease-in-out; outline: none; transition: 0.25s all ease-in-out;`, + primaryButtonNoBorder: `background-color: #ffffff; color: #7c4dff; font-family: ${styleConstants.fontFamilyButton}; border-radius: 0.25em; border-color: #7c4dff; border: 1px solid; cursor: pointer; font-size: .8em; text-decoration: none; padding: 0.5em 4em; transition: 0.25s all ease-in-out; outline: none;`, + primaryButtonNoBorderHover: `background-color: #7c4dff; color: #ffffff; font-family: ${styleConstants.fontFamilyButton}; border-radius: 0.25em; border-color: #7c4dff; border: 1px solid; cursor: pointer; font-size: .8em; text-decoration: none; padding: 0.5em 4em; transition: 0.25s all ease-in-out; outline: none; transition: 0.25s all ease-in-out;`, + secondaryButton: `background-color: #01c9ea; color: #ffffff; font-family: ${styleConstants.fontFamilyButton}; border-radius: 0.25em; border-color: #01c9ea; border: 1px solid; cursor: pointer; font-size: .8em; text-decoration: none; padding: 0.5em 4em; transition: 0.25s all ease-in-out; outline: none;`, + secondaryButtonHover: `background-color: #37cde6; color: #ffffff; font-family: ${styleConstants.fontFamilyButton}; border-radius: 0.25em; border-color: #7c4dff; border: 1px solid; cursor: pointer; font-size: .8em; text-decoration: none; padding: 0.5em 4em; transition: 0.25s all ease-in-out; outline: none; transition: 0.25s all ease-in-out;`, + secondaryButtonNoBorder: `background-color: #ffffff; color: #01c9ea; font-family: ${styleConstants.fontFamilyButton}; border-radius: 0.25em; border-color: #01c9ea; border: 1px solid; cursor: pointer; font-size: .8em; text-decoration: none; padding: 0.5em 4em; transition: 0.25s all ease-in-out; outline: none;`, + secondaryButtonNoBorderHover: `background-color: #01c9ea; color: #ffffff; font-family: ${styleConstants.fontFamilyButton}; border-radius: 0.25em; border-color: #01c9ea; border: 1px solid; cursor: pointer; font-size: .8em; text-decoration: none; padding: 0.5em 4em; transition: 0.25s all ease-in-out; outline: none; transition: 0.25s all ease-in-out;`, // media controlStyle: `border-radius: 0.5em; margin: 0.8em; width:${styleConstants.mediaModuleCanvasWidth}; height:${styleConstants.mediaModuleCanvasHeight};`, diff --git a/src/styleConstants.js b/src/styleConstants.js index ff4a4b8b4..74b869a4f 100644 --- a/src/styleConstants.js +++ b/src/styleConstants.js @@ -1,6 +1,10 @@ export default { highlightColor: '#7C4DFF', // Solid lavender https://design.inrupt.com/atomic-core/?cat=Core + // Font families referenced throughout the UI + fontFamilyButton: 'Raleway, Roboto, sans-serif', // Used for primary and secondary buttons + fontFamilyHeader: 'Arial, sans-serif', // Used for header menus and navigation + formBorderColor: '#888888', // Mid-grey formHeadingColor: '#888888', // originally was brown; now grey lowProfileLinkColor: '#3B5998', // Grey-blue, e.g., for field labels linking to ontology diff --git a/test/unit/fonts.test.ts b/test/unit/fonts.test.ts new file mode 100644 index 000000000..88ef6da0c --- /dev/null +++ b/test/unit/fonts.test.ts @@ -0,0 +1,62 @@ +import { fontNames, fontFamilies, loadFonts } from '../../src/fonts' + +describe('fontNames', () => { + it('exposes the primary button font name', () => { + expect(fontNames.button).toEqual('Raleway') + }) + + it('exposes the button fallback font name', () => { + expect(fontNames.buttonFallback).toEqual('Roboto') + }) +}) + +describe('fontFamilies', () => { + it('includes the button font family string', () => { + expect(typeof fontFamilies.button).toEqual('string') + expect(fontFamilies.button).toContain('Raleway') + expect(fontFamilies.button).toContain('Roboto') + expect(fontFamilies.button).toContain('sans-serif') + }) + + it('includes the header font family string', () => { + expect(typeof fontFamilies.header).toEqual('string') + expect(fontFamilies.header).toContain('Arial') + expect(fontFamilies.header).toContain('sans-serif') + }) +}) + +describe('loadFonts', () => { + beforeEach(() => { + // Clear any elements added by previous tests + document.querySelectorAll('link[data-solid-ui-fonts]').forEach(el => el.remove()) + }) + + it('appends a element to document.head', () => { + loadFonts() + const link = document.querySelector('link[data-solid-ui-fonts]') + expect(link).not.toBeNull() + expect(link!.getAttribute('rel')).toEqual('stylesheet') + }) + + it('uses the default Google Fonts URL when no argument is supplied', () => { + loadFonts() + const link = document.querySelector('link[data-solid-ui-fonts]') as HTMLLinkElement + expect(link.href).toContain('fonts.googleapis.com') + expect(link.href).toContain('Raleway') + expect(link.href).toContain('Roboto') + }) + + it('accepts a custom font URL', () => { + const customUrl = 'https://example.com/fonts.css' + loadFonts(customUrl) + const link = document.querySelector('link[data-solid-ui-fonts]') as HTMLLinkElement + expect(link.href).toEqual(customUrl) + }) + + it('does not add duplicate elements when called multiple times', () => { + loadFonts() + loadFonts() + const links = document.querySelectorAll('link[data-solid-ui-fonts]') + expect(links.length).toEqual(1) + }) +}) diff --git a/test/unit/header/__snapshots__/index.test.ts.snap b/test/unit/header/__snapshots__/index.test.ts.snap index e788acc96..a25ea5456 100644 --- a/test/unit/header/__snapshots__/index.test.ts.snap +++ b/test/unit/header/__snapshots__/index.test.ts.snap @@ -97,7 +97,7 @@ exports[`createBanner check customized logo... 1`] = ` > @@ -119,7 +119,7 @@ exports[`createBanner check customized logo... 1`] = ` > @@ -235,7 +235,7 @@ exports[`createBanner creates a link 1`] = ` > @@ -257,7 +257,7 @@ exports[`createBanner creates a link 1`] = ` > @@ -407,7 +407,7 @@ exports[`createUserMenu creates a menu.... 1`] = ` > @@ -417,7 +417,7 @@ exports[`createUserMenu creates a menu.... 1`] = ` exports[`createUserMenuButton creates a button 1`] = `