Skip to content

Commit

Permalink
feat(standard): add theme color meta (#772)
Browse files Browse the repository at this point in the history
* feat(standard): add theme color meta

* perf: reduce code by merging empty array case into if clause

* perf: reduce code by updating keyval meta def factory

* docs: add theme color to comparison page
  • Loading branch information
davidlj95 authored Aug 13, 2024
1 parent 09087ee commit 3205f08
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 3 deletions.
11 changes: 11 additions & 0 deletions projects/ngx-meta/api-extractor/ngx-meta.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export const makeComposedKeyValMetaDefinition: (names: ReadonlyArray<string>, op
export const makeKeyValMetaDefinition: (keyName: string, options?: {
keyAttr?: string;
valAttr?: string;
extras?: MetaDefinition;
}) => NgxMetaMetaDefinition;

// @internal (undocumented)
Expand Down Expand Up @@ -442,6 +443,7 @@ export interface Standard {
readonly generator?: true | null;
readonly keywords?: ReadonlyArray<string> | null;
readonly locale?: GlobalMetadata['locale'];
readonly themeColor?: StandardThemeColorMetadata | null;
readonly title?: GlobalMetadata['title'];
}

Expand Down Expand Up @@ -480,6 +482,15 @@ export interface StandardMetadata {
standard: Standard;
}

// @public
export type StandardThemeColorMetadata = string | ReadonlyArray<StandardThemeColorMetadataObject>;

// @public
export interface StandardThemeColorMetadataObject {
color: string;
media?: string;
}

// @public
export const TWITTER_CARD_CARD_METADATA_PROVIDER: FactoryProvider;

Expand Down
1 change: 1 addition & 0 deletions projects/ngx-meta/docs/content/why/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ It is certainly a better option than installing a poorly maintained library. But
| `#!html <meta name="keywords">` |||||
| `#!html <meta name="generator">` || ⚙️ | ⚙️ | ⚙️ |
| `#!html <meta name="application-name">` || ⚙️ | ⚙️ | ⚙️ |
| `#!html <meta name="theme-color">` || ⚙️ | ⚙️ | ⚙️ |
| `#!html <link rel="canonical">` |||||
| `#!html <html lang>` |||||
| `#!html <meta property="og:title">` |||||
Expand Down
3 changes: 2 additions & 1 deletion projects/ngx-meta/e2e/cypress/fixtures/all-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"standard": {
"author": "Mr Bar",
"keywords": ["foo", "bar"],
"generator": true
"generator": true,
"themeColor": "blue"
},
"openGraph": {
"image": {
Expand Down
4 changes: 4 additions & 0 deletions projects/ngx-meta/e2e/cypress/support/metadata/standard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ export const shouldContainAllStandardMetadata = () =>
.should('have.attr', 'href')
.and('eq', metadata.canonicalUrl)
cy.get('html').should('have.attr', 'lang').and('eq', metadata.locale)
cy.getMeta('theme-color')
.shouldHaveContent()
.and('eq', metadata.standard.themeColor)
},
)
})
Expand All @@ -45,4 +48,5 @@ export const shouldNotContainAnyStandardMetadata = () =>
cy.getMeta('application-name').should('not.exist')
cy.get('link[rel="canonical"]').should('not.exist')
cy.get('html').should('not.have.attr', 'lang')
cy.getMeta('theme-color').should('not.exist')
})
11 changes: 9 additions & 2 deletions projects/ngx-meta/src/core/src/make-key-val-meta-definition.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NgxMetaMetaDefinition } from './ngx-meta-meta.service'
import { MetaDefinition } from '@angular/platform-browser'

/**
* Creates a {@link NgxMetaMetaDefinition} for its use with {@link NgxMetaMetaService}
Expand All @@ -19,9 +20,10 @@ import { NgxMetaMetaDefinition } from './ngx-meta-meta.service'
*
* @param keyName - Name of the key in the key/value meta definition
* @param options - Specifies HTML attribute defining key, HTML attribute defining
* value.
* value and optional extras to include in definition
* `keyAttr` defaults to `name`
* `valAttr` defaults to `content`
* `extras` defaults to nothing
*
* @public
*/
Expand All @@ -30,12 +32,17 @@ export const makeKeyValMetaDefinition = (
options: {
keyAttr?: string
valAttr?: string
extras?: MetaDefinition
} = {},
): NgxMetaMetaDefinition => {
const keyAttr = options.keyAttr ?? _KEY_ATTRIBUTE_NAME
const valAttr = options.valAttr ?? _VAL_ATTRIBUTE_CONTENT
return {
withContent: (value) => ({ [keyAttr]: keyName, [valAttr]: value }),
withContent: (value) => ({
[keyAttr]: keyName,
[valAttr]: value,
...options.extras,
}),
attrSelector: `${keyAttr}='${keyName}'`,
}
}
Expand Down
1 change: 1 addition & 0 deletions projects/ngx-meta/src/standard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './src/provide-ngx-meta-standard'
// Others
export * from './src/standard'
export * from './src/standard-metadata'
export * from './src/standard-theme-color-metadata'
// Specific providers
export * from './src/standard-title-metadata-provider'
export * from './src/standard-description-metadata-provider'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { STANDARD_GENERATOR_METADATA_PROVIDER } from './standard-generator-metad
import { STANDARD_APPLICATION_NAME_METADATA_PROVIDER } from './standard-application-name-metadata-provider'
import { STANDARD_CANONICAL_URL_METADATA_PROVIDER } from './standard-canonical-url-metadata-provider'
import { STANDARD_LOCALE_METADATA_PROVIDER } from './standard-locale-metadata-provider'
import { STANDARD_THEME_COLOR_METADATA_PROVIDER } from './standard-theme-color-metadata-provider'

/**
* Adds {@link https://ngx-meta.dev/built-in-modules/standard/ | standard module}
Expand All @@ -25,4 +26,5 @@ export const provideNgxMetaStandard = (): Provider[] => [
STANDARD_APPLICATION_NAME_METADATA_PROVIDER,
STANDARD_CANONICAL_URL_METADATA_PROVIDER,
STANDARD_LOCALE_METADATA_PROVIDER,
STANDARD_THEME_COLOR_METADATA_PROVIDER,
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { MetadataSetter, NgxMetaMetaService } from '../../core'
import { Standard } from './standard'
import { TestBed } from '@angular/core/testing'
import { MockProvider } from 'ng-mocks'
import {
__STANDARD_THEME_COLOR_METADATA_SETTER_FACTORY,
_STANDARD_THEME_COLOR_KEY,
} from './standard-theme-color-metadata-provider'
import { enableAutoSpy } from '@/ngx-meta/test/enable-auto-spy'
import { MetaDefinition } from '@angular/platform-browser'
import { StandardThemeColorMetadataObject } from './standard-theme-color-metadata'

describe('Standard theme color metadata', () => {
enableAutoSpy()
let sut: MetadataSetter<Standard[typeof _STANDARD_THEME_COLOR_KEY]>
let metaService: jasmine.SpyObj<NgxMetaMetaService>

const DUMMY_COLOR = 'black'
const DUMMY_MEDIA = '(prefers-color-scheme: dark)'

beforeEach(() => {
sut = makeSut()
metaService = TestBed.inject(
NgxMetaMetaService,
) as jasmine.SpyObj<NgxMetaMetaService>
})

it('should call the meta service with no value when no value provided', () => {
sut(undefined)

expect(metaService.set).toHaveBeenCalledOnceWith(
jasmine.anything(),
undefined,
)
})

it('should call the meta service with the specified content when a string value is provided', () => {
sut(DUMMY_COLOR)

expect(metaService.set).toHaveBeenCalledOnceWith(
jasmine.anything(),
DUMMY_COLOR,
)
})

it('should call the meta service with no value when an empty array is provided', () => {
sut([])

expect(metaService.set).toHaveBeenCalledOnceWith(
jasmine.anything(),
undefined,
)
})

it('should call the meta service with the specified content and media when object values are provided', () => {
const firstMediaDefinition = {
color: DUMMY_COLOR,
media: DUMMY_MEDIA,
} satisfies MetaDefinition & StandardThemeColorMetadataObject
const secondMediaDefinition = {
color: 'white',
} satisfies MetaDefinition & StandardThemeColorMetadataObject
sut([firstMediaDefinition, secondMediaDefinition])

expect(metaService.set).toHaveBeenCalledWith(
jasmine.anything(),
firstMediaDefinition.color,
)
expect(metaService.set).toHaveBeenCalledWith(
jasmine.anything(),
secondMediaDefinition.color,
)
expect(
metaService.set.calls
.argsFor(0)[0]
.withContent(firstMediaDefinition.color),
).toEqual({
name: 'theme-color',
content: firstMediaDefinition.color,
media: firstMediaDefinition.media,
})
expect(
metaService.set.calls
.argsFor(1)[0]
.withContent(secondMediaDefinition.color),
).toEqual({
name: 'theme-color',
content: secondMediaDefinition.color,
})
})
})

function makeSut(): MetadataSetter<Standard[typeof _STANDARD_THEME_COLOR_KEY]> {
TestBed.configureTestingModule({
providers: [MockProvider(NgxMetaMetaService)],
})
return __STANDARD_THEME_COLOR_METADATA_SETTER_FACTORY(
TestBed.inject(NgxMetaMetaService),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { makeStandardMetadataProvider } from './make-standard-metadata-provider'
import {
MetadataSetterFactory,
NgxMetaMetaService,
} from '@davidlj95/ngx-meta/core'
import { Standard } from './standard'
import { makeStandardMetaDefinition } from './make-standard-meta-definition'
import { StandardThemeColorMetadataObject } from './standard-theme-color-metadata'

/**
* @internal
*/
export const _STANDARD_THEME_COLOR_KEY = 'themeColor' satisfies keyof Standard

const META_NAME = 'theme-color'

/**
* @internal
*/
export const __STANDARD_THEME_COLOR_METADATA_SETTER_FACTORY: MetadataSetterFactory<
Standard[typeof _STANDARD_THEME_COLOR_KEY]
> = (ngxMetaMetaService: NgxMetaMetaService) => (value) => {
const isValueAnArray = isStandardThemeColorArray(value)
const baseMetaDefinition = makeStandardMetaDefinition(META_NAME)
if (!value || !isValueAnArray || !value.length) {
ngxMetaMetaService.set(
baseMetaDefinition,
isValueAnArray ? undefined : value,
)
return
}
for (const { media, color } of value) {
ngxMetaMetaService.set(
makeStandardMetaDefinition(META_NAME, media ? { extras: { media } } : {}),
color,
)
}
}

const isStandardThemeColorArray = (
value: Standard['themeColor'],
): value is ReadonlyArray<StandardThemeColorMetadataObject> =>
Array.isArray(value)

/**
* Manages the {@link Standard.themeColor} metadata
* @public
*/
export const STANDARD_THEME_COLOR_METADATA_PROVIDER =
makeStandardMetadataProvider(_STANDARD_THEME_COLOR_KEY, {
s: __STANDARD_THEME_COLOR_METADATA_SETTER_FACTORY,
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* See {@link Standard.themeColor}
* @public
*/
export type StandardThemeColorMetadata =
| string
| ReadonlyArray<StandardThemeColorMetadataObject>

/**
* See {@link Standard.themeColor}
* @public
*/
export interface StandardThemeColorMetadataObject {
/**
* See {@link Standard.themeColor}
*/
color: string
/**
* See {@link Standard.themeColor}
*/
media?: string
}
22 changes: 22 additions & 0 deletions projects/ngx-meta/src/standard/src/standard.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { GlobalMetadata } from '@davidlj95/ngx-meta/core'
import { StandardThemeColorMetadata } from './standard-theme-color-metadata'

/**
* {@link https://ngx-meta.dev/built-in-modules/standard/ | Standard module}
Expand Down Expand Up @@ -82,4 +83,25 @@ export interface Standard {
* @see https://html.spec.whatwg.org/multipage/dom.html#attr-lang
*/
readonly locale?: GlobalMetadata['locale']

/**
* Sets one or more `<meta name='theme-color'>` HTML elements
*
* If set, colors must specify a valid CSS color.
*
* A `media` attribute can be set to specify a different color depending on
* the context based on a CSS media query. For instance, to provide one color
* for dark mode and another for light mode.
*
* You can use a `string` value to set one theme color as value. No `media`
* attribute will be used.
*
* You can also specify one or more colors & media queries combinations by
* providing an array of objects specifying the color and (optionally) a
* media query
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name/theme-color
* @see https://html.spec.whatwg.org/multipage/semantics.html#meta-theme-color
*/
readonly themeColor?: StandardThemeColorMetadata | null
}

0 comments on commit 3205f08

Please sign in to comment.