folk-canvas/labs/intl-elements/intl-number.ts

163 lines
5.3 KiB
TypeScript

import { FolkElement } from '@lib';
import { css } from '@lit/reactive-element';
import { property } from '@lit/reactive-element/decorators.js';
declare global {
interface HTMLElementTagNameMap {
'intl-number': IntlNumber;
}
}
type NumberFormatOptions = Intl.NumberFormatOptions;
// Ported from https://github.com/elematic/heximal/blob/main/packages/components/src/lib/num.ts
export class IntlNumber extends FolkElement {
static tagName = 'intl-number';
static override styles = css`
slot {
display: none;
}
`;
// `lang` is a global attribute, should be navigate up the DOM tree to find it?
@property({ reflect: true }) locale: string | undefined;
// Locale options
@property({ reflect: true }) localeMatcher: NumberFormatOptions['localeMatcher'];
@property({ reflect: true }) numberingSystem: NumberFormatOptions['numberingSystem'];
// Digit options
@property({ reflect: true, type: Number }) minimumIntegerDigits: NumberFormatOptions['minimumIntegerDigits'];
@property({ reflect: true, type: Number }) minimumFractionDigits: NumberFormatOptions['minimumFractionDigits'];
@property({ reflect: true, type: Number }) maximumFractionDigits: NumberFormatOptions['maximumFractionDigits'];
@property({ reflect: true, type: Number }) minimumSignificantDigits: NumberFormatOptions['minimumSignificantDigits'];
@property({ reflect: true, type: Number }) maximumSignificantDigits: NumberFormatOptions['maximumSignificantDigits'];
@property({ reflect: true }) roundingPriority: NumberFormatOptions['roundingPriority'];
@property({ reflect: true, type: Number }) roundingIncrement: NumberFormatOptions['roundingIncrement'];
@property({ reflect: true }) roundingMode: NumberFormatOptions['roundingMode'];
@property({ reflect: true }) trailingZeroDisplay: NumberFormatOptions['trailingZeroDisplay'];
// Style options
// There is a name collision with the style property, so call it display instead.
@property({ reflect: true }) display: NumberFormatOptions['style'];
@property({ reflect: true }) currency: NumberFormatOptions['currency'];
@property({ reflect: true }) currencyDisplay: NumberFormatOptions['currencyDisplay'];
@property({ reflect: true }) currencySign: NumberFormatOptions['currencySign'];
@property({ reflect: true }) unit: NumberFormatOptions['unit'];
@property({ reflect: true }) unitDisplay: NumberFormatOptions['unitDisplay'];
// Other options
@property({ reflect: true }) notation: NumberFormatOptions['notation'];
@property({ reflect: true }) compactDisplay: NumberFormatOptions['compactDisplay'];
@property({ reflect: true }) useGrouping: NumberFormatOptions['useGrouping'];
@property({ reflect: true }) signDisplay: NumberFormatOptions['signDisplay'];
#format: Intl.NumberFormat | undefined;
#value: number = NaN;
#slot = document.createElement('slot');
#span = document.createElement('span');
get value() {
return this.#value;
}
set value(value) {
this.#value = value;
this.#updateValue();
}
get formattedValue() {
return this.#span.textContent;
}
override createRenderRoot(): HTMLElement | DocumentFragment {
const root = super.createRenderRoot();
this.#span.part.add('number');
this.#slot.addEventListener('slotchange', () => {
this.value = parseFloat(this.textContent?.trim() || '');
});
root.append(this.#slot, this.#span);
return root;
}
override connectedCallback(): void {
super.connectedCallback();
window.addEventListener('languagechange', this.#onLanguageChange);
}
override disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener('languagechange', this.#onLanguageChange);
}
#onLanguageChange = () => this.requestUpdate();
override willUpdate(): void {
// Any change to properties requires re-creating the formatter.
// Default locale to navigator.language since it's the browsers language setting
// Passing undefined seems to reflect the OS's language setting.
this.#format = new Intl.NumberFormat(this.locale || navigator.language, {
localeMatcher: this.localeMatcher,
numberingSystem: this.numberingSystem,
minimumIntegerDigits: this.minimumIntegerDigits,
minimumFractionDigits: this.minimumFractionDigits,
maximumFractionDigits: this.maximumFractionDigits,
minimumSignificantDigits: this.minimumSignificantDigits,
maximumSignificantDigits: this.maximumSignificantDigits,
roundingPriority: this.roundingPriority,
roundingIncrement: this.roundingIncrement,
roundingMode: this.roundingMode,
trailingZeroDisplay: this.trailingZeroDisplay,
// If there are is no style attribute, try to infer it.
style: this.display ?? (this.currency ? 'currency' : this.unit ? 'unit' : 'decimal'),
currency: this.currency,
currencyDisplay: this.currencyDisplay,
currencySign: this.currencySign,
unit: this.unit,
unitDisplay: this.unitDisplay,
notation: this.notation,
compactDisplay: this.compactDisplay,
useGrouping: this.useGrouping,
signDisplay: this.signDisplay,
});
this.#updateValue();
}
#updateValue = () => {
if (Number.isNaN(this.#value)) {
this.#span.textContent = '';
} else if (this.#format) {
this.#span.textContent = this.#format.format(this.#value);
}
};
}