@oicl/openbridge-webcomponents-full-bundle 2.0.0-next.114 → 2.0.0-next.116
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/openbridge-webcomponents.bundle.js +29 -8
- package/bundle/openbridge-webcomponents.bundle.js.map +1 -1
- package/custom-elements.json +21 -4
- package/dist/building-blocks/readout-block/readout-block.d.ts.map +1 -1
- package/dist/building-blocks/readout-block/readout-block.js +5 -2
- package/dist/building-blocks/readout-block/readout-block.js.map +1 -1
- package/dist/components/text-input-field/text-input-field.d.ts +1 -1
- package/dist/components/text-input-field/text-input-field.d.ts.map +1 -1
- package/dist/components/text-input-field/text-input-field.js +5 -2
- package/dist/components/text-input-field/text-input-field.js.map +1 -1
- package/dist/components/toggle-switch/toggle-switch.d.ts +16 -1
- package/dist/components/toggle-switch/toggle-switch.d.ts.map +1 -1
- package/dist/components/toggle-switch/toggle-switch.js +6 -1
- package/dist/components/toggle-switch/toggle-switch.js.map +1 -1
- package/dist/navigation-instruments/readout/readout-formatters.d.ts +23 -1
- package/dist/navigation-instruments/readout/readout-formatters.d.ts.map +1 -1
- package/dist/navigation-instruments/readout/readout-formatters.js +6 -4
- package/dist/navigation-instruments/readout/readout-formatters.js.map +1 -1
- package/dist/navigation-instruments/readout/readout-shared.d.ts.map +1 -1
- package/dist/navigation-instruments/readout/readout-shared.js +9 -0
- package/dist/navigation-instruments/readout/readout-shared.js.map +1 -1
- package/package.json +1 -1
- package/src/building-blocks/readout-block/readout-block.stories.ts +243 -1
- package/src/building-blocks/readout-block/readout-block.ts +5 -1
- package/src/components/text-input-field/text-input-field.spec.ts +15 -0
- package/src/components/text-input-field/text-input-field.ts +6 -3
- package/src/components/toggle-switch/toggle-switch.spec.ts +31 -0
- package/src/components/toggle-switch/toggle-switch.ts +24 -2
- package/src/navigation-instruments/readout/readout-formatters.spec.ts +143 -3
- package/src/navigation-instruments/readout/readout-formatters.ts +43 -5
- package/src/navigation-instruments/readout/readout-shared.spec.ts +13 -0
- package/src/navigation-instruments/readout/readout-shared.ts +16 -0
- package/src/navigation-instruments/readout/readout.stories.ts +110 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"readout-formatters.js","sources":["../../../src/navigation-instruments/readout/readout-formatters.ts"],"sourcesContent":["export type ReadoutNumericFormatOptions = {\n showZeroPadding: boolean;\n minValueLength: number;\n fractionDigits: number;\n};\n\n/**\n * How a readout's `value` is interpreted.\n * - `number`: formatted via `fractionDigits` / `maxDigits`.\n * - `text`: rendered verbatim, with the numeric format options ignored.\n */\nexport enum ReadoutValueType {\n number = 'number',\n text = 'text',\n}\n\nconst READOUT_VALUE_TYPES: readonly string[] = Object.values(ReadoutValueType);\n\n/** Whether `value` is one of the supported {@link ReadoutValueType} values. */\nexport function isReadoutValueType(value: unknown): value is ReadoutValueType {\n return typeof value === 'string' && READOUT_VALUE_TYPES.includes(value);\n}\n\nfunction isBlank(value: string): boolean {\n return value.trim() === '';\n}\n\n/**\n * Throws when `valueType` is not a supported value, or when `value` is text but\n * `valueType` is `number`.\n *\n * `valueType` is validated first because an attribute carries an unchecked\n * string: a typo such as `valuetype=\"strng\"` matches neither mode, so every\n * mode check falls through and the readout silently renders the unavailable\n * dash — the opposite of the loud failure this contract exists to give.\n * `undefined`/`null` are allowed and mean \"use the default\".\n *\n * Attributes are always strings, so a numeric-looking string (`value=\"10.12\"`)\n * is accepted and parsed. Blank strings resolve to the unavailable dash rather\n * than throwing — `value=\"${maybeUndefined}\"` is a common template shape, and\n * `Number('')` is `0`, a silently wrong reading.\n */\nexport function assertReadoutValueType(\n tagName: string,\n value: number | string | null | undefined,\n valueType: ReadoutValueType\n): void {\n // `undefined`/`null` mean \"use the default\", matching how the components and\n // the resolvers treat an unset `valueType`.\n const resolved = valueType ?? ReadoutValueType.number;\n if (!isReadoutValueType(resolved)) {\n throw new TypeError(\n `<${tagName}>: valueType must be ` +\n `${READOUT_VALUE_TYPES.map((t) => `\"${t}\"`).join(' or ')} ` +\n `(got ${JSON.stringify(valueType)}).`\n );\n }\n if (\n resolved !== ReadoutValueType.number ||\n typeof value !== 'string' ||\n isBlank(value)\n ) {\n return;\n }\n if (Number.isFinite(Number(value))) {\n return;\n }\n throw new TypeError(\n `<${tagName}>: value must be a number when valueType is \"number\" ` +\n `(got ${JSON.stringify(value)}). Set valueType=\"text\" to render text.`\n );\n}\n\n
|
|
1
|
+
{"version":3,"file":"readout-formatters.js","sources":["../../../src/navigation-instruments/readout/readout-formatters.ts"],"sourcesContent":["export type ReadoutNumericFormatOptions = {\n showZeroPadding: boolean;\n minValueLength: number;\n fractionDigits: number;\n};\n\n/**\n * How a readout's `value` is interpreted.\n * - `number`: formatted via `fractionDigits` / `maxDigits`.\n * - `text`: rendered verbatim, with the numeric format options ignored.\n */\nexport enum ReadoutValueType {\n number = 'number',\n text = 'text',\n}\n\nconst READOUT_VALUE_TYPES: readonly string[] = Object.values(ReadoutValueType);\n\n/** Whether `value` is one of the supported {@link ReadoutValueType} values. */\nexport function isReadoutValueType(value: unknown): value is ReadoutValueType {\n return typeof value === 'string' && READOUT_VALUE_TYPES.includes(value);\n}\n\nfunction isBlank(value: string): boolean {\n return value.trim() === '';\n}\n\n/**\n * Throws when `valueType` is not a supported value, or when `value` is text but\n * `valueType` is `number`.\n *\n * `valueType` is validated first because an attribute carries an unchecked\n * string: a typo such as `valuetype=\"strng\"` matches neither mode, so every\n * mode check falls through and the readout silently renders the unavailable\n * dash — the opposite of the loud failure this contract exists to give.\n * `undefined`/`null` are allowed and mean \"use the default\".\n *\n * Attributes are always strings, so a numeric-looking string (`value=\"10.12\"`)\n * is accepted and parsed. Blank strings resolve to the unavailable dash rather\n * than throwing — `value=\"${maybeUndefined}\"` is a common template shape, and\n * `Number('')` is `0`, a silently wrong reading.\n */\nexport function assertReadoutValueType(\n tagName: string,\n value: number | string | null | undefined,\n valueType: ReadoutValueType\n): void {\n // `undefined`/`null` mean \"use the default\", matching how the components and\n // the resolvers treat an unset `valueType`.\n const resolved = valueType ?? ReadoutValueType.number;\n if (!isReadoutValueType(resolved)) {\n throw new TypeError(\n `<${tagName}>: valueType must be ` +\n `${READOUT_VALUE_TYPES.map((t) => `\"${t}\"`).join(' or ')} ` +\n `(got ${JSON.stringify(valueType)}).`\n );\n }\n if (\n resolved !== ReadoutValueType.number ||\n typeof value !== 'string' ||\n isBlank(value)\n ) {\n return;\n }\n if (Number.isFinite(Number(value))) {\n return;\n }\n throw new TypeError(\n `<${tagName}>: value must be a number when valueType is \"number\" ` +\n `(got ${JSON.stringify(value)}). Set valueType=\"text\" to render text.`\n );\n}\n\n/**\n * The value as a number, or `undefined` when it is text / unavailable.\n *\n * A non-finite number (`NaN`, `±Infinity`) counts as unavailable and renders the\n * dash, the same as `null`. `NaN` is a runtime data condition — a sensor\n * dropout, a `0/0`, a bad parse — not a programmer error, so it must not throw;\n * and `value.toFixed()` would otherwise render the literal text `\"NaN\"` /\n * `\"Infinity\"` in place of a reading. This also makes the number path agree with\n * the string path below, which has always resolved a non-finite string to\n * `undefined` — before this, `<obc-readout value=\"NaN\">` rendered a dash while\n * `.value=${NaN}` rendered `\"NaN\"`.\n */\nexport function resolveReadoutNumericValue(\n value: number | string | null | undefined,\n valueType: ReadoutValueType\n): number | undefined {\n if (\n valueType === ReadoutValueType.text ||\n value === null ||\n value === undefined\n ) {\n return undefined;\n }\n if (typeof value === 'number') {\n return Number.isFinite(value) ? value : undefined;\n }\n if (isBlank(value)) {\n return undefined;\n }\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n}\n\n/** The value as display text, or `undefined` when not in text mode / blank. */\nexport function resolveReadoutTextValue(\n value: number | string | null | undefined,\n valueType: ReadoutValueType\n): string | undefined {\n if (\n valueType !== ReadoutValueType.text ||\n value === null ||\n value === undefined\n ) {\n return undefined;\n }\n const text = typeof value === 'number' ? String(value) : value;\n return isBlank(text) ? undefined : text;\n}\n\n/**\n * The character used for an unavailable (\"no reading\") value.\n *\n * U+2012 FIGURE DASH, not the ASCII hyphen-minus: it is defined to be the same\n * width as a digit, so the placeholder lines up with the reading it stands in\n * for. Measured in Noto Sans with tabular figures at `size=\"m\"` — digit 13.02px,\n * U+2012 13.02px, U+002D 7.02px, en dash 11.02px, em dash 22.02px. With a\n * hyphen, `-.--` sat 46% narrow per character and its decimal point missed the\n * reading's; with U+2012 the point and every fraction position align exactly.\n */\nexport const READOUT_UNAVAILABLE_DASH = '\\u2012';\n\n/**\n * The unavailable (\"no reading\") text: a single integer dash plus one dash per\n * fraction digit — `\\u2012` at `fractionDigits` 0, `\\u2012.\\u2012\\u2012` at 2.\n *\n * Deliberately NOT filled out to `maxDigits`: the placeholder stays short and\n * sits at the right edge of the reserved width, rather than spelling out every\n * reserved digit position. `maxDigits` still reserves the width, so nothing\n * shifts when a reading arrives.\n */\nfunction dashedGenerator({\n showZeroPadding,\n minValueLength,\n fractionDigits,\n}: ReadoutNumericFormatOptions): string {\n const visibleDigits = showZeroPadding ? Math.max(minValueLength, 1) : 1;\n\n if (fractionDigits < 1) {\n return READOUT_UNAVAILABLE_DASH.repeat(visibleDigits);\n }\n\n const integerDigits = visibleDigits - fractionDigits;\n\n return (\n READOUT_UNAVAILABLE_DASH.repeat(Math.max(integerDigits, 1)) +\n '.' +\n READOUT_UNAVAILABLE_DASH.repeat(fractionDigits)\n );\n}\n\nexport function formatNumericValue(\n value: number | undefined,\n options: ReadoutNumericFormatOptions\n): string {\n // Non-finite counts as unavailable here too, not only in\n // `resolveReadoutNumericValue`. Every caller normalises today, but this\n // function is exported, and `NaN.toFixed()` would put the literal text\n // \"NaN\" where a reading belongs — the exact failure this change removes.\n if (value === undefined || !Number.isFinite(value)) {\n return dashedGenerator(options);\n }\n\n return value.toFixed(options.fractionDigits);\n}\n\nexport function readoutFormattedInteger(valueText: string): number {\n const t = valueText.trim();\n if (!t) {\n return 0;\n }\n\n const rest = t.startsWith('-') ? t.slice(1) : t;\n const dot = rest.indexOf('.');\n return dot === -1 ? rest.length : dot;\n}\n\nexport function getHintZeros(\n value: number | undefined,\n {showZeroPadding, minValueLength, fractionDigits}: ReadoutNumericFormatOptions\n): string {\n const formattedValue = formatNumericValue(value, {\n showZeroPadding,\n minValueLength,\n fractionDigits,\n });\n const dotLength = fractionDigits > 0 ? 1 : 0;\n const integerLength = formattedValue.length - dotLength;\n const hintedDigits = Math.max(minValueLength - integerLength, 0);\n\n if (hintedDigits > 0) {\n return '0'.repeat(hintedDigits);\n }\n\n return '';\n}\n"],"names":["ReadoutValueType"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"readout-shared.d.ts","sourceRoot":"","sources":["../../../src/navigation-instruments/readout/readout-shared.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,oBAAoB,EACrB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,qBAAqB,EACtB,MAAM,sDAAsD,CAAC;AAC9D,OAAO,EAEL,KAAK,2BAA2B,EACjC,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACrB,2BAA2B,
|
|
1
|
+
{"version":3,"file":"readout-shared.d.ts","sourceRoot":"","sources":["../../../src/navigation-instruments/readout/readout-shared.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,oBAAoB,EACrB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,qBAAqB,EACtB,MAAM,sDAAsD,CAAC;AAC9D,OAAO,EAEL,KAAK,2BAA2B,EACjC,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACrB,2BAA2B,CAY7B;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,MAAM,GAAG,IAAI,EACpB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,aAAa,EAAE,2BAA2B,GACzC,OAAO,CAkBT;AAED,wDAAwD;AACxD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,GAAG,cAAc,CASzE;AAED,0EAA0E;AAC1E,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,gBAAgB,GAAG,cAAc,CAS3E;AAED,4EAA4E;AAC5E,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,OAAO,GAClB,oBAAoB,CAItB;AAED,8EAA8E;AAC9E,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,uBAAuB,GAAG,SAAS,GAC/C,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAKzB;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,eAAe,EAAE,OAAO,EACxB,aAAa,EAAE,qBAAqB,GACnC,qBAAqB,CAEvB"}
|
|
@@ -3,6 +3,12 @@ import { ReadoutBlockSize, ReadoutBlockDataQuality, ReadoutBlockHidePhase } from
|
|
|
3
3
|
import { formatNumericValue } from "./readout-formatters.js";
|
|
4
4
|
function readoutNumericFormatOptions(maxDigits, fractionDigits) {
|
|
5
5
|
return {
|
|
6
|
+
// Comparison-only (see `isDisplayedAtSetpoint`), which returns early unless
|
|
7
|
+
// both operands are finite numbers — so `formatNumericValue` never reaches
|
|
8
|
+
// its unavailable-dash branch here and the padding would have no effect.
|
|
9
|
+
// `obc-readout-block` sets it to `false` for rendering as well, since the
|
|
10
|
+
// unavailable placeholder is deliberately short rather than filled out to
|
|
11
|
+
// `maxDigits`. Nothing enables it today.
|
|
6
12
|
showZeroPadding: false,
|
|
7
13
|
minValueLength: maxDigits,
|
|
8
14
|
fractionDigits
|
|
@@ -12,6 +18,9 @@ function isDisplayedAtSetpoint(value, setpoint, formatOptions) {
|
|
|
12
18
|
if (value === null || setpoint === void 0) {
|
|
13
19
|
return false;
|
|
14
20
|
}
|
|
21
|
+
if (!Number.isFinite(value) || !Number.isFinite(setpoint)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
15
24
|
return formatNumericValue(value, formatOptions) === formatNumericValue(setpoint, formatOptions);
|
|
16
25
|
}
|
|
17
26
|
function readoutPrimarySize(size) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"readout-shared.js","sources":["../../../src/navigation-instruments/readout/readout-shared.ts"],"sourcesContent":["import {\n ObcTextboxSize,\n ObcTextboxFontWeight,\n} from '../../components/textbox/textbox.js';\nimport {\n ReadoutBlockSize,\n ReadoutBlockDataQuality,\n ReadoutBlockHidePhase,\n} from '../../building-blocks/readout-block/readout-block.js';\nimport {\n formatNumericValue,\n type ReadoutNumericFormatOptions,\n} from './readout-formatters.js';\n\n/**\n * Pure helpers shared by `obc-readout` and `obc-readout-list-item`. The two\n * components are layout siblings built from the same primitives + per-block\n * options API (see #1012); this module keeps their behavioral logic in\n * lock-step until an eventual merge.\n *\n * ## Features\n * - **Numeric formatting:** {@link readoutNumericFormatOptions} builds the\n * shared `maxDigits`/`fractionDigits` format options (never zero-padded).\n * - **Setpoint comparison:** {@link isDisplayedAtSetpoint} compares the\n * DISPLAYED (rounded) value and setpoint strings, not the raw numbers.\n * - **Typography mapping:** {@link readoutPrimarySize} /\n * {@link readoutSecondarySize} map the density tier to the primary /\n * de-emphasised `obc-textbox` size; {@link readoutSetpointWeight} picks the\n * setpoint font weight from its emphasis state.\n * - **Data quality:** {@link readoutDataQualityClasses} produces the classMap\n * fragment for the low-integrity / invalid chip.\n * - **Pop-up hide phase:** {@link resolveSetpointHidePhase} maps the\n * component's deferred-hide state onto the block-level\n * `ReadoutBlockHidePhase`.\n *\n * ## Usage Guidelines\n * These helpers are consumed by the components' private getters — behavioral\n * changes belong here (once), not re-inlined per component.\n *\n * @example\n * const format = readoutNumericFormatOptions(3, 1);\n * const atSetpoint = isDisplayedAtSetpoint(29.99, 30, format);\n */\n\nexport function readoutNumericFormatOptions(\n maxDigits: number,\n fractionDigits: number\n): ReadoutNumericFormatOptions {\n return {\n showZeroPadding: false,\n minValueLength: maxDigits,\n fractionDigits,\n };\n}\n\n/**\n * Whether the value reads as \"at the setpoint\". Compares what is DISPLAYED\n * (rounded to fractionDigits), not the raw values, so e.g. 29.999 and 30 at\n * fractionDigits=0 both read \"30\" → at setpoint.\n */\nexport function isDisplayedAtSetpoint(\n value: number | null,\n setpoint: number | undefined,\n formatOptions: ReadoutNumericFormatOptions\n): boolean {\n if (value === null || setpoint === undefined) {\n return false;\n }\n return (\n formatNumericValue(value, formatOptions) ===\n formatNumericValue(setpoint, formatOptions)\n );\n}\n\n/** Primary value-typography size for a density tier. */\nexport function readoutPrimarySize(size: ReadoutBlockSize): ObcTextboxSize {\n switch (size) {\n case ReadoutBlockSize.large:\n return ObcTextboxSize.l;\n case ReadoutBlockSize.medium:\n return ObcTextboxSize.m;\n default:\n return ObcTextboxSize.s;\n }\n}\n\n/** Secondary (de-emphasised) value-typography size for a density tier. */\nexport function readoutSecondarySize(size: ReadoutBlockSize): ObcTextboxSize {\n switch (size) {\n case ReadoutBlockSize.large:\n return ObcTextboxSize.s;\n case ReadoutBlockSize.medium:\n return ObcTextboxSize.s;\n default:\n return ObcTextboxSize.xs;\n }\n}\n\n/** Setpoint is SemiBold only while emphasised, otherwise regular weight. */\nexport function readoutSetpointWeight(\n emphasized: boolean\n): ObcTextboxFontWeight {\n return emphasized\n ? ObcTextboxFontWeight.semibold\n : ObcTextboxFontWeight.regular;\n}\n\n/** classMap fragment for a block / source carrying per-block data quality. */\nexport function readoutDataQualityClasses(\n dataQuality: ReadoutBlockDataQuality | undefined\n): Record<string, boolean> {\n return {\n 'data-low-integrity': dataQuality === ReadoutBlockDataQuality.lowIntegrity,\n 'data-invalid': dataQuality === ReadoutBlockDataQuality.invalid,\n };\n}\n\n/**\n * The block-level hide phase for a pop-up setpoint: the component's deferred\n * phase while the value sits at the setpoint, `none` otherwise (value away\n * from the setpoint, or touching).\n */\nexport function resolveSetpointHidePhase(\n popUpAtSetpoint: boolean,\n deferredPhase: ReadoutBlockHidePhase\n): ReadoutBlockHidePhase {\n return popUpAtSetpoint ? deferredPhase : ReadoutBlockHidePhase.none;\n}\n"],"names":[],"mappings":";;;AA4CO,SAAS,4BACd,WACA,gBAC6B;AAC7B,SAAO;AAAA,
|
|
1
|
+
{"version":3,"file":"readout-shared.js","sources":["../../../src/navigation-instruments/readout/readout-shared.ts"],"sourcesContent":["import {\n ObcTextboxSize,\n ObcTextboxFontWeight,\n} from '../../components/textbox/textbox.js';\nimport {\n ReadoutBlockSize,\n ReadoutBlockDataQuality,\n ReadoutBlockHidePhase,\n} from '../../building-blocks/readout-block/readout-block.js';\nimport {\n formatNumericValue,\n type ReadoutNumericFormatOptions,\n} from './readout-formatters.js';\n\n/**\n * Pure helpers shared by `obc-readout` and `obc-readout-list-item`. The two\n * components are layout siblings built from the same primitives + per-block\n * options API (see #1012); this module keeps their behavioral logic in\n * lock-step until an eventual merge.\n *\n * ## Features\n * - **Numeric formatting:** {@link readoutNumericFormatOptions} builds the\n * shared `maxDigits`/`fractionDigits` format options (never zero-padded).\n * - **Setpoint comparison:** {@link isDisplayedAtSetpoint} compares the\n * DISPLAYED (rounded) value and setpoint strings, not the raw numbers.\n * - **Typography mapping:** {@link readoutPrimarySize} /\n * {@link readoutSecondarySize} map the density tier to the primary /\n * de-emphasised `obc-textbox` size; {@link readoutSetpointWeight} picks the\n * setpoint font weight from its emphasis state.\n * - **Data quality:** {@link readoutDataQualityClasses} produces the classMap\n * fragment for the low-integrity / invalid chip.\n * - **Pop-up hide phase:** {@link resolveSetpointHidePhase} maps the\n * component's deferred-hide state onto the block-level\n * `ReadoutBlockHidePhase`.\n *\n * ## Usage Guidelines\n * These helpers are consumed by the components' private getters — behavioral\n * changes belong here (once), not re-inlined per component.\n *\n * @example\n * const format = readoutNumericFormatOptions(3, 1);\n * const atSetpoint = isDisplayedAtSetpoint(29.99, 30, format);\n */\n\nexport function readoutNumericFormatOptions(\n maxDigits: number,\n fractionDigits: number\n): ReadoutNumericFormatOptions {\n return {\n // Comparison-only (see `isDisplayedAtSetpoint`), which returns early unless\n // both operands are finite numbers — so `formatNumericValue` never reaches\n // its unavailable-dash branch here and the padding would have no effect.\n // `obc-readout-block` sets it to `false` for rendering as well, since the\n // unavailable placeholder is deliberately short rather than filled out to\n // `maxDigits`. Nothing enables it today.\n showZeroPadding: false,\n minValueLength: maxDigits,\n fractionDigits,\n };\n}\n\n/**\n * Whether the value reads as \"at the setpoint\". Compares what is DISPLAYED\n * (rounded to fractionDigits), not the raw values, so e.g. 29.999 and 30 at\n * fractionDigits=0 both read \"30\" → at setpoint.\n */\nexport function isDisplayedAtSetpoint(\n value: number | null,\n setpoint: number | undefined,\n formatOptions: ReadoutNumericFormatOptions\n): boolean {\n if (value === null || setpoint === undefined) {\n return false;\n }\n // An unavailable reading is never \"at\" the setpoint. Callers normalise\n // `value` (via `resolveReadoutNumericValue`) but pass `setpoint` raw, so a\n // non-finite setpoint would otherwise be formatted as the literal \"NaN\" /\n // \"Infinity\" and compared as a string. The comparison result happens to be\n // correct either way — a normalised `value` can never also format to \"NaN\" —\n // but guarding both keeps the two operands symmetric and the invariant below\n // honest.\n if (!Number.isFinite(value) || !Number.isFinite(setpoint)) {\n return false;\n }\n return (\n formatNumericValue(value, formatOptions) ===\n formatNumericValue(setpoint, formatOptions)\n );\n}\n\n/** Primary value-typography size for a density tier. */\nexport function readoutPrimarySize(size: ReadoutBlockSize): ObcTextboxSize {\n switch (size) {\n case ReadoutBlockSize.large:\n return ObcTextboxSize.l;\n case ReadoutBlockSize.medium:\n return ObcTextboxSize.m;\n default:\n return ObcTextboxSize.s;\n }\n}\n\n/** Secondary (de-emphasised) value-typography size for a density tier. */\nexport function readoutSecondarySize(size: ReadoutBlockSize): ObcTextboxSize {\n switch (size) {\n case ReadoutBlockSize.large:\n return ObcTextboxSize.s;\n case ReadoutBlockSize.medium:\n return ObcTextboxSize.s;\n default:\n return ObcTextboxSize.xs;\n }\n}\n\n/** Setpoint is SemiBold only while emphasised, otherwise regular weight. */\nexport function readoutSetpointWeight(\n emphasized: boolean\n): ObcTextboxFontWeight {\n return emphasized\n ? ObcTextboxFontWeight.semibold\n : ObcTextboxFontWeight.regular;\n}\n\n/** classMap fragment for a block / source carrying per-block data quality. */\nexport function readoutDataQualityClasses(\n dataQuality: ReadoutBlockDataQuality | undefined\n): Record<string, boolean> {\n return {\n 'data-low-integrity': dataQuality === ReadoutBlockDataQuality.lowIntegrity,\n 'data-invalid': dataQuality === ReadoutBlockDataQuality.invalid,\n };\n}\n\n/**\n * The block-level hide phase for a pop-up setpoint: the component's deferred\n * phase while the value sits at the setpoint, `none` otherwise (value away\n * from the setpoint, or touching).\n */\nexport function resolveSetpointHidePhase(\n popUpAtSetpoint: boolean,\n deferredPhase: ReadoutBlockHidePhase\n): ReadoutBlockHidePhase {\n return popUpAtSetpoint ? deferredPhase : ReadoutBlockHidePhase.none;\n}\n"],"names":[],"mappings":";;;AA4CO,SAAS,4BACd,WACA,gBAC6B;AAC7B,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOL,iBAAiB;AAAA,IACjB,gBAAgB;AAAA,IAChB;AAAA,EAAA;AAEJ;AAOO,SAAS,sBACd,OACA,UACA,eACS;AACT,MAAI,UAAU,QAAQ,aAAa,QAAW;AAC5C,WAAO;AAAA,EACT;AAQA,MAAI,CAAC,OAAO,SAAS,KAAK,KAAK,CAAC,OAAO,SAAS,QAAQ,GAAG;AACzD,WAAO;AAAA,EACT;AACA,SACE,mBAAmB,OAAO,aAAa,MACvC,mBAAmB,UAAU,aAAa;AAE9C;AAGO,SAAS,mBAAmB,MAAwC;AACzE,UAAQ,MAAA;AAAA,IACN,KAAK,iBAAiB;AACpB,aAAO,eAAe;AAAA,IACxB,KAAK,iBAAiB;AACpB,aAAO,eAAe;AAAA,IACxB;AACE,aAAO,eAAe;AAAA,EAAA;AAE5B;AAGO,SAAS,qBAAqB,MAAwC;AAC3E,UAAQ,MAAA;AAAA,IACN,KAAK,iBAAiB;AACpB,aAAO,eAAe;AAAA,IACxB,KAAK,iBAAiB;AACpB,aAAO,eAAe;AAAA,IACxB;AACE,aAAO,eAAe;AAAA,EAAA;AAE5B;AAGO,SAAS,sBACd,YACsB;AACtB,SAAO,aACH,qBAAqB,WACrB,qBAAqB;AAC3B;AAGO,SAAS,0BACd,aACyB;AACzB,SAAO;AAAA,IACL,sBAAsB,gBAAgB,wBAAwB;AAAA,IAC9D,gBAAgB,gBAAgB,wBAAwB;AAAA,EAAA;AAE5D;AAOO,SAAS,yBACd,iBACA,eACuB;AACvB,SAAO,kBAAkB,gBAAgB,sBAAsB;AACjE;"}
|
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
ReadoutValueType,
|
|
11
11
|
} from './readout-block.js';
|
|
12
12
|
import './readout-block.js';
|
|
13
|
+
import '../../components/textbox/textbox.js';
|
|
13
14
|
import '../../icons/icon-placeholder.js';
|
|
14
15
|
import {
|
|
15
16
|
ObcAlertFrameMode,
|
|
@@ -41,11 +42,18 @@ type BlockArgs = {
|
|
|
41
42
|
// A faithful single-block render. The block inherits its colour from the host
|
|
42
43
|
// context (the list-item normally drives it), so standalone it shows the neutral
|
|
43
44
|
// default tone; `enhanced` switches to the accent tone.
|
|
45
|
+
//
|
|
46
|
+
// `value` is passed through as given, deliberately NOT as `args.value ?? null`:
|
|
47
|
+
// one unavailable case IS `undefined`, and coercing here would quietly turn it
|
|
48
|
+
// into the `null` case. `undefined` sits outside the declared `value` type —
|
|
49
|
+
// consumers should pass `null` — but an unset property arrives as `undefined` in
|
|
50
|
+
// practice and has to read the same, so the cast is what the case is testing
|
|
51
|
+
// rather than a way around the type checker.
|
|
44
52
|
function renderBlock(args: Partial<BlockArgs>) {
|
|
45
53
|
return html`
|
|
46
54
|
<obc-readout-block
|
|
47
55
|
.variant=${args.variant ?? ReadoutBlockVariant.value}
|
|
48
|
-
.value=${args.value
|
|
56
|
+
.value=${args.value as number | string | null}
|
|
49
57
|
.valueType=${args.valueType ?? ReadoutValueType.number}
|
|
50
58
|
.size=${args.size ?? ReadoutBlockSize.small}
|
|
51
59
|
.enhanced=${args.enhanced ?? false}
|
|
@@ -415,6 +423,240 @@ export const TestValidationSurvivesUnrelatedUpdate: Story = {
|
|
|
415
423
|
},
|
|
416
424
|
};
|
|
417
425
|
|
|
426
|
+
// The designer's specification, revised in review: the unavailable placeholder
|
|
427
|
+
// stays SHORT (`-.--`, not `---.--`) and sits at the right of the reserved width.
|
|
428
|
+
// format: 000.00 · readout: 12.30 · hinted: 012.30 · not available: -.--
|
|
429
|
+
// `format: 000.00` maps to maxDigits 3 + fractionDigits 2.
|
|
430
|
+
const DESIGNER_SPEC_CASES: {
|
|
431
|
+
label: string;
|
|
432
|
+
expected: string;
|
|
433
|
+
args: Partial<BlockArgs>;
|
|
434
|
+
}[] = [
|
|
435
|
+
{
|
|
436
|
+
label: 'readout',
|
|
437
|
+
expected: '12.30',
|
|
438
|
+
args: {value: 12.3, maxDigits: 3, fractionDigits: 2},
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
label: 'readout with hinted',
|
|
442
|
+
expected: '012.30',
|
|
443
|
+
args: {value: 12.3, maxDigits: 3, fractionDigits: 2, hintedZeros: true},
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
label: 'not available',
|
|
447
|
+
expected: '-.--',
|
|
448
|
+
args: {value: null, maxDigits: 3, fractionDigits: 2},
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
label: 'not available, hinted enabled',
|
|
452
|
+
expected: '-.--',
|
|
453
|
+
args: {value: null, maxDigits: 3, fractionDigits: 2, hintedZeros: true},
|
|
454
|
+
},
|
|
455
|
+
];
|
|
456
|
+
|
|
457
|
+
// Every input that reads as "no reading". All four of the first rows are the
|
|
458
|
+
// same placeholder — that IS the point: whichever way a reading goes missing,
|
|
459
|
+
// the readout looks identical.
|
|
460
|
+
const UNAVAILABLE_CASES: {
|
|
461
|
+
label: string;
|
|
462
|
+
args: Partial<BlockArgs>;
|
|
463
|
+
}[] = [
|
|
464
|
+
{
|
|
465
|
+
label: 'NaN',
|
|
466
|
+
args: {value: Number.NaN, maxDigits: 3, fractionDigits: 2},
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
label: 'Infinity',
|
|
470
|
+
args: {value: Number.POSITIVE_INFINITY, maxDigits: 3, fractionDigits: 2},
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
label: 'null',
|
|
474
|
+
args: {value: null, maxDigits: 3, fractionDigits: 2},
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
label: 'undefined — an unset value, same as null',
|
|
478
|
+
args: {value: undefined, maxDigits: 3, fractionDigits: 2},
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
label: 'null · no fraction digits',
|
|
482
|
+
args: {value: null, maxDigits: 3},
|
|
483
|
+
},
|
|
484
|
+
];
|
|
485
|
+
|
|
486
|
+
// Stacked in one column under identical settings so the decimal points and the
|
|
487
|
+
// fraction positions can be checked against each other by eye.
|
|
488
|
+
const ALIGNMENT_CASES: Partial<BlockArgs>[] = [
|
|
489
|
+
{value: 12.3, maxDigits: 3, fractionDigits: 2},
|
|
490
|
+
{value: 4.5, maxDigits: 3, fractionDigits: 2},
|
|
491
|
+
{value: null, maxDigits: 3, fractionDigits: 2},
|
|
492
|
+
{value: Number.NaN, maxDigits: 3, fractionDigits: 2},
|
|
493
|
+
];
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* **Unavailable values — for design review.**
|
|
497
|
+
*
|
|
498
|
+
* 1. **The placeholder is short.** `-.--` for `format: 000.00`, not `---.--`:
|
|
499
|
+
* `maxDigits` already reserves the width, so the dash sits at the right edge
|
|
500
|
+
* of the reserve rather than spelling out every reserved digit position.
|
|
501
|
+
* 2. **The dash is digit-width.** It is U+2012 FIGURE DASH, not the ASCII
|
|
502
|
+
* hyphen-minus. Measured in Noto Sans with tabular figures, a digit is
|
|
503
|
+
* 13.02px and U+2012 is 13.02px, while U+002D is 7.02px — so with a hyphen
|
|
504
|
+
* the placeholder's decimal point missed the reading's. `tabular-nums` does
|
|
505
|
+
* not help here: it equalises figures with each other and leaves punctuation
|
|
506
|
+
* untouched (measured identical with the feature on and off).
|
|
507
|
+
* 3. **`NaN` and `±Infinity` count as unavailable.** They previously rendered as
|
|
508
|
+
* the literal text `NaN` / `Infinity` in place of a reading. They are a
|
|
509
|
+
* runtime data condition (sensor dropout, `0/0`, a bad parse) rather than a
|
|
510
|
+
* programmer error, so they resolve to the dash rather than throwing.
|
|
511
|
+
*
|
|
512
|
+
* Hinted zeros are suppressed for an unavailable value, so the two "not
|
|
513
|
+
* available" rows are identical and nothing reads `----Na.N`.
|
|
514
|
+
*
|
|
515
|
+
* **Open question — dash treatment.** U+2012 is exactly digit-width, which is
|
|
516
|
+
* what makes the columns line up, but two adjacent dashes then run together
|
|
517
|
+
* into one bar. The last section renders the alternatives at a readable size:
|
|
518
|
+
* (A) the figure dash as implemented, (B) an ASCII hyphen centred in a
|
|
519
|
+
* digit-width cell — separated and still aligned, at the cost of a lighter,
|
|
520
|
+
* sparser look — and (C) the previous plain hyphen, for reference. Delete that
|
|
521
|
+
* section once the choice is made.
|
|
522
|
+
*/
|
|
523
|
+
export const UnavailableValues: Story = {
|
|
524
|
+
render: () => html`
|
|
525
|
+
<style>
|
|
526
|
+
.rb-unavail {
|
|
527
|
+
display: grid;
|
|
528
|
+
grid-template-columns: max-content max-content max-content;
|
|
529
|
+
gap: 8px 24px;
|
|
530
|
+
align-items: center;
|
|
531
|
+
}
|
|
532
|
+
.rb-unavail-2col {
|
|
533
|
+
grid-template-columns: max-content max-content;
|
|
534
|
+
}
|
|
535
|
+
.rb-unavail-head {
|
|
536
|
+
font: 10px/1.2 var(--global-typography-ui-label-font-family, sans-serif);
|
|
537
|
+
text-transform: uppercase;
|
|
538
|
+
letter-spacing: 0.06em;
|
|
539
|
+
color: var(--element-neutral-color, #777);
|
|
540
|
+
}
|
|
541
|
+
.rb-unavail-label {
|
|
542
|
+
font: 12px/1.4 var(--global-typography-ui-label-font-family, sans-serif);
|
|
543
|
+
color: var(--element-neutral-color, #777);
|
|
544
|
+
}
|
|
545
|
+
.rb-unavail-spec {
|
|
546
|
+
margin: 20px 0 8px;
|
|
547
|
+
font: 12px/1.2 var(--global-typography-ui-label-font-family, sans-serif);
|
|
548
|
+
text-transform: uppercase;
|
|
549
|
+
letter-spacing: 0.06em;
|
|
550
|
+
color: var(--element-neutral-color, #777);
|
|
551
|
+
}
|
|
552
|
+
.rb-unavail-spec-value {
|
|
553
|
+
font:
|
|
554
|
+
13px/1.4 ui-monospace,
|
|
555
|
+
monospace;
|
|
556
|
+
color: var(--element-neutral-color, #777);
|
|
557
|
+
}
|
|
558
|
+
.rb-unavail-now {
|
|
559
|
+
outline: 1px dashed rgba(0, 0, 0, 0.12);
|
|
560
|
+
width: max-content;
|
|
561
|
+
}
|
|
562
|
+
.rb-dash-options {
|
|
563
|
+
display: flex;
|
|
564
|
+
flex-direction: column;
|
|
565
|
+
gap: 8px;
|
|
566
|
+
width: max-content;
|
|
567
|
+
}
|
|
568
|
+
.rb-cellspan {
|
|
569
|
+
display: inline-block;
|
|
570
|
+
width: 1ch;
|
|
571
|
+
text-align: center;
|
|
572
|
+
}
|
|
573
|
+
/* One column, so the decimal points can be compared down the stack. */
|
|
574
|
+
.rb-align {
|
|
575
|
+
display: flex;
|
|
576
|
+
flex-direction: column;
|
|
577
|
+
align-items: flex-start;
|
|
578
|
+
gap: 2px;
|
|
579
|
+
width: max-content;
|
|
580
|
+
outline: 1px dashed rgba(0, 0, 0, 0.12);
|
|
581
|
+
}
|
|
582
|
+
</style>
|
|
583
|
+
<div class="rb-unavail-spec">Designer specification — format 000.00</div>
|
|
584
|
+
<div class="rb-unavail">
|
|
585
|
+
<div class="rb-unavail-head">Case</div>
|
|
586
|
+
<div class="rb-unavail-head">Specified</div>
|
|
587
|
+
<div class="rb-unavail-head">Rendered</div>
|
|
588
|
+
${DESIGNER_SPEC_CASES.map(
|
|
589
|
+
(c) => html`
|
|
590
|
+
<div class="rb-unavail-label">${c.label}</div>
|
|
591
|
+
<div class="rb-unavail-spec-value">${c.expected}</div>
|
|
592
|
+
<div class="rb-unavail-now">
|
|
593
|
+
${renderBlock({size: ReadoutBlockSize.medium, ...c.args})}
|
|
594
|
+
</div>
|
|
595
|
+
`
|
|
596
|
+
)}
|
|
597
|
+
</div>
|
|
598
|
+
|
|
599
|
+
<div class="rb-unavail-spec">
|
|
600
|
+
Alignment — decimal points and fraction positions line up
|
|
601
|
+
</div>
|
|
602
|
+
<div class="rb-align">
|
|
603
|
+
${ALIGNMENT_CASES.map((args) =>
|
|
604
|
+
renderBlock({size: ReadoutBlockSize.medium, ...args})
|
|
605
|
+
)}
|
|
606
|
+
</div>
|
|
607
|
+
|
|
608
|
+
<div class="rb-unavail-spec">
|
|
609
|
+
Dash treatment — open question for the designer
|
|
610
|
+
</div>
|
|
611
|
+
<div class="rb-dash-options">
|
|
612
|
+
<div>
|
|
613
|
+
<div class="rb-unavail-label">reading, for reference</div>
|
|
614
|
+
<obc-textbox size="l" .tabularNums=${true}>12.30</obc-textbox>
|
|
615
|
+
</div>
|
|
616
|
+
<div>
|
|
617
|
+
<div class="rb-unavail-label">
|
|
618
|
+
A — U+2012 figure dash (implemented): digit-width, but adjacent dashes
|
|
619
|
+
run together
|
|
620
|
+
</div>
|
|
621
|
+
<obc-textbox size="l" .tabularNums=${true}
|
|
622
|
+
>‒.‒‒</obc-textbox
|
|
623
|
+
>
|
|
624
|
+
</div>
|
|
625
|
+
<div>
|
|
626
|
+
<div class="rb-unavail-label">
|
|
627
|
+
B — hyphen in digit-width cells: separated, but lighter and sparser
|
|
628
|
+
</div>
|
|
629
|
+
<obc-textbox size="l" .tabularNums=${true}>
|
|
630
|
+
<span class="rb-cellspan">-</span>.<span class="rb-cellspan">-</span
|
|
631
|
+
><span class="rb-cellspan">-</span>
|
|
632
|
+
</obc-textbox>
|
|
633
|
+
</div>
|
|
634
|
+
<div>
|
|
635
|
+
<div class="rb-unavail-label">
|
|
636
|
+
C — ASCII hyphen (previous): narrower than a digit, does not align
|
|
637
|
+
</div>
|
|
638
|
+
<obc-textbox size="l" .tabularNums=${true}>-.--</obc-textbox>
|
|
639
|
+
</div>
|
|
640
|
+
</div>
|
|
641
|
+
|
|
642
|
+
<div class="rb-unavail-spec">
|
|
643
|
+
Unavailable values — every unreadable input
|
|
644
|
+
</div>
|
|
645
|
+
<div class="rb-unavail rb-unavail-2col">
|
|
646
|
+
<div class="rb-unavail-head">Value</div>
|
|
647
|
+
<div class="rb-unavail-head">Rendered</div>
|
|
648
|
+
${UNAVAILABLE_CASES.map(
|
|
649
|
+
(c) => html`
|
|
650
|
+
<div class="rb-unavail-label">${c.label}</div>
|
|
651
|
+
<div class="rb-unavail-now">
|
|
652
|
+
${renderBlock({size: ReadoutBlockSize.medium, ...c.args})}
|
|
653
|
+
</div>
|
|
654
|
+
`
|
|
655
|
+
)}
|
|
656
|
+
</div>
|
|
657
|
+
`,
|
|
658
|
+
};
|
|
659
|
+
|
|
418
660
|
export const DataQuality: Story = {
|
|
419
661
|
render: () =>
|
|
420
662
|
renderShowcase([
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
assertReadoutValueType,
|
|
18
18
|
resolveReadoutNumericValue,
|
|
19
19
|
resolveReadoutTextValue,
|
|
20
|
+
READOUT_UNAVAILABLE_DASH,
|
|
20
21
|
ReadoutValueType,
|
|
21
22
|
type ReadoutNumericFormatOptions,
|
|
22
23
|
} from '../../navigation-instruments/readout/readout-formatters.js';
|
|
@@ -215,6 +216,9 @@ export class ObcReadoutBlock extends LitElement {
|
|
|
215
216
|
|
|
216
217
|
private get numericFormatOptions(): ReadoutNumericFormatOptions {
|
|
217
218
|
return {
|
|
219
|
+
// The unavailable placeholder stays short (`\u2012.\u2012\u2012`) rather than
|
|
220
|
+
// spelling out every reserved digit position — `maxDigits` already
|
|
221
|
+
// reserves the width, so it simply sits at the right edge of it.
|
|
218
222
|
showZeroPadding: false,
|
|
219
223
|
minValueLength: this.maxDigits,
|
|
220
224
|
fractionDigits: this.fractionDigits,
|
|
@@ -334,7 +338,7 @@ export class ObcReadoutBlock extends LitElement {
|
|
|
334
338
|
const text = this.off
|
|
335
339
|
? this.offText
|
|
336
340
|
: isTextMode
|
|
337
|
-
? (textValue ??
|
|
341
|
+
? (textValue ?? READOUT_UNAVAILABLE_DASH)
|
|
338
342
|
: formatNumericValue(valueForFormat, formatOptions);
|
|
339
343
|
// Hinted zeros pad the INTEGER part up to `maxDigits`, independent of
|
|
340
344
|
// `fractionDigits` (the decimal point and fraction digits never count toward
|
|
@@ -173,6 +173,21 @@ describe('obc-text-input-field', () => {
|
|
|
173
173
|
|
|
174
174
|
expect(changeHandler).toHaveBeenCalled();
|
|
175
175
|
});
|
|
176
|
+
|
|
177
|
+
it('carries the committed value in the change detail', async () => {
|
|
178
|
+
const changeHandler = vi.fn();
|
|
179
|
+
el.addEventListener('change', changeHandler);
|
|
180
|
+
|
|
181
|
+
input.value = 'new value';
|
|
182
|
+
input.dispatchEvent(new InputEvent('input', {bubbles: true}));
|
|
183
|
+
input.dispatchEvent(new Event('change', {bubbles: true}));
|
|
184
|
+
await el.updateComplete;
|
|
185
|
+
|
|
186
|
+
const event = changeHandler.mock.calls[0][0] as CustomEvent<{
|
|
187
|
+
value: string;
|
|
188
|
+
}>;
|
|
189
|
+
expect(event.detail).toEqual({value: 'new value'});
|
|
190
|
+
});
|
|
176
191
|
});
|
|
177
192
|
|
|
178
193
|
describe('password visibility', () => {
|
|
@@ -58,7 +58,7 @@ export enum ObcTextInputFieldPlacement {
|
|
|
58
58
|
* @slot label-icon - Icon displayed before the label text (when `hasLabelIcon` is true)
|
|
59
59
|
* @slot helper-icon - Icon displayed before helper or error text (when `hasHelperIcon` is true)
|
|
60
60
|
* @fires input - Standard input event on value change
|
|
61
|
-
* @fires change -
|
|
61
|
+
* @fires change - {CustomEvent<{value: string}>} Dispatched on value change, carrying the committed value
|
|
62
62
|
* @fires clear - Fired when the clear button is clicked
|
|
63
63
|
* @fires blur - Fired when the input field is blurred
|
|
64
64
|
* @stable
|
|
@@ -198,8 +198,11 @@ export class ObcTextInputField extends LitElement {
|
|
|
198
198
|
</div>`;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
private fireChangeEvent() {
|
|
202
|
-
|
|
201
|
+
private fireChangeEvent(e: Event) {
|
|
202
|
+
const target = e.target as HTMLInputElement;
|
|
203
|
+
this.dispatchEvent(
|
|
204
|
+
new CustomEvent('change', {detail: {value: target.value}})
|
|
205
|
+
);
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
private get shouldUpdateValue(): boolean {
|
|
@@ -53,6 +53,20 @@ describe('obc-toggle-switch', () => {
|
|
|
53
53
|
expect(event.detail).toEqual({checked: true});
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
it('dispatches change event with checked detail', async () => {
|
|
57
|
+
const handler = vi.fn();
|
|
58
|
+
el.addEventListener('change', handler);
|
|
59
|
+
|
|
60
|
+
input.click();
|
|
61
|
+
await el.updateComplete;
|
|
62
|
+
|
|
63
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
64
|
+
const event = handler.mock.calls[0]?.[0] as CustomEvent<{
|
|
65
|
+
checked: boolean;
|
|
66
|
+
}>;
|
|
67
|
+
expect(event.detail).toEqual({checked: true});
|
|
68
|
+
});
|
|
69
|
+
|
|
56
70
|
it('combination of click and js control', async () => {
|
|
57
71
|
el.checked = true;
|
|
58
72
|
await el.updateComplete;
|
|
@@ -116,6 +130,23 @@ describe('obc-toggle-switch', () => {
|
|
|
116
130
|
}>;
|
|
117
131
|
expect(event.detail).toEqual({checked: true});
|
|
118
132
|
});
|
|
133
|
+
|
|
134
|
+
it('change event reports the user-selected state even when no input listener responds', async () => {
|
|
135
|
+
const handler = vi.fn();
|
|
136
|
+
el.addEventListener('change', handler);
|
|
137
|
+
|
|
138
|
+
input.click();
|
|
139
|
+
await el.updateComplete;
|
|
140
|
+
|
|
141
|
+
// The host state stays unchanged (controlled), but the change event
|
|
142
|
+
// must report the user's selection, consistent with the input event.
|
|
143
|
+
expect(el.checked).toBe(false);
|
|
144
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
145
|
+
const event = handler.mock.calls[0]?.[0] as CustomEvent<{
|
|
146
|
+
checked: boolean;
|
|
147
|
+
}>;
|
|
148
|
+
expect(event.detail).toEqual({checked: true});
|
|
149
|
+
});
|
|
119
150
|
});
|
|
120
151
|
|
|
121
152
|
describe('disabled', () => {
|
|
@@ -9,6 +9,10 @@ export type ObcToggleSwitchInputEvent = CustomEvent<{
|
|
|
9
9
|
checked: boolean;
|
|
10
10
|
}>;
|
|
11
11
|
|
|
12
|
+
export type ObcToggleSwitchChangeEvent = CustomEvent<{
|
|
13
|
+
checked: boolean;
|
|
14
|
+
}>;
|
|
15
|
+
|
|
12
16
|
/**
|
|
13
17
|
* `<obc-toggle-switch>` – A toggle switch component for binary on/off selection (also known as a switch, toggle, or enable/disable control).
|
|
14
18
|
*
|
|
@@ -56,6 +60,12 @@ export type ObcToggleSwitchInputEvent = CustomEvent<{
|
|
|
56
60
|
*
|
|
57
61
|
* ### Events
|
|
58
62
|
* - `input` – Fired when the toggle state changes (checked/unchecked).
|
|
63
|
+
* - `change` – Fired when the toggle state changes by user interaction.
|
|
64
|
+
*
|
|
65
|
+
* Both events report the state the user selected. When `externalControl` is
|
|
66
|
+
* true the component does not update itself; the consumer decides whether to
|
|
67
|
+
* apply the reported state to `checked` and may reject the interaction by
|
|
68
|
+
* leaving `checked` unchanged.
|
|
59
69
|
*
|
|
60
70
|
* ---
|
|
61
71
|
*
|
|
@@ -85,7 +95,7 @@ export type ObcToggleSwitchInputEvent = CustomEvent<{
|
|
|
85
95
|
*
|
|
86
96
|
* @slot icon - Leading icon slot (shown when `hasIcon` is true)
|
|
87
97
|
* @fires input - {ObcToggleSwitchInputEvent} Dispatched when the value of the input changes
|
|
88
|
-
* @fires change - Dispatched when the value of the input changes by user interaction
|
|
98
|
+
* @fires change - {ObcToggleSwitchChangeEvent} Dispatched when the value of the input changes by user interaction
|
|
89
99
|
* @stable
|
|
90
100
|
*/
|
|
91
101
|
@customElement('obc-toggle-switch')
|
|
@@ -136,6 +146,13 @@ export class ObcToggleSwitch extends LitElement {
|
|
|
136
146
|
*/
|
|
137
147
|
@property({type: Boolean}) externalControl = false;
|
|
138
148
|
|
|
149
|
+
/**
|
|
150
|
+
* The state the user selected in the most recent interaction. In
|
|
151
|
+
* externalControl mode this can differ from `checked` until the consumer
|
|
152
|
+
* accepts the change, so the change event reports it instead of `checked`.
|
|
153
|
+
*/
|
|
154
|
+
private _userSelectedChecked?: boolean;
|
|
155
|
+
|
|
139
156
|
/**
|
|
140
157
|
* Handles input events to change the toggle state.
|
|
141
158
|
* Prevents changes if the toggle is disabled.
|
|
@@ -149,6 +166,7 @@ export class ObcToggleSwitch extends LitElement {
|
|
|
149
166
|
}
|
|
150
167
|
|
|
151
168
|
const nextChecked = !this.checked;
|
|
169
|
+
this._userSelectedChecked = nextChecked;
|
|
152
170
|
if (!this.externalControl) {
|
|
153
171
|
this.checked = nextChecked;
|
|
154
172
|
}
|
|
@@ -169,7 +187,11 @@ export class ObcToggleSwitch extends LitElement {
|
|
|
169
187
|
e.preventDefault();
|
|
170
188
|
return;
|
|
171
189
|
}
|
|
172
|
-
this.dispatchEvent(
|
|
190
|
+
this.dispatchEvent(
|
|
191
|
+
new CustomEvent('change', {
|
|
192
|
+
detail: {checked: this._userSelectedChecked ?? this.checked},
|
|
193
|
+
})
|
|
194
|
+
);
|
|
173
195
|
}
|
|
174
196
|
|
|
175
197
|
override render() {
|