From 6be5c263b822a44f7d3e71b183c9c8f2bb19362a Mon Sep 17 00:00:00 2001 From: "Ronald A. Richardson" Date: Tue, 18 Aug 2026 17:33:28 +0800 Subject: [PATCH] Replace ember-radio-button with a native implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drops the prerelease dependency. `ember-radio-button` was pinned to 3.0.0-beta.1 because its last stable release, 2.0.1, references the removed `Ember` global and throws on Ember 5 — and the package has not been updated in four years. Adds `radio-button` and `radio-button-input` to the addon, keeping the component names, arguments and rendered DOM identical to the upstream addon, including the `ember-radio-button` label class that existing stylesheets target. The two call sites — modal/layouts/option-prompt and custom-field/input — are unchanged, and their tests pass untouched. The full surface is preserved: @value, @groupValue, @changed, @name, @disabled, @required, @autofocus, @tabindex, @radioClass, @radioId, @classNames (string or array), @checkedClass, @ariaLabelledby and @ariaDescribedby; block form wraps the input in a label, inline form renders the input alone. Selection is compared with `isEqual` and reported through `once`, both as upstream, so equality semantics and callback timing are unchanged. radio-button.hbs carries a scoped template-lint disable for no-autofocus-attribute and no-positive-tabindex. Both rules target an author hardcoding those values; here they are pass-through bindings, and removing them would change the component's argument list. 20 tests, both components at 100% coverage. 4964 tests pass, 0 skips. `pnpm run build` exits 0. --- addon/components/radio-button-input.hbs | 1 + addon/components/radio-button-input.js | 33 +++ addon/components/radio-button.hbs | 40 ++++ addon/components/radio-button.js | 51 +++++ app/components/radio-button-input.js | 1 + app/components/radio-button.js | 1 + package.json | 1 - pnpm-lock.yaml | 28 --- .../components/radio-button-test.js | 210 ++++++++++++++++++ 9 files changed, 337 insertions(+), 29 deletions(-) create mode 100644 addon/components/radio-button-input.hbs create mode 100644 addon/components/radio-button-input.js create mode 100644 addon/components/radio-button.hbs create mode 100644 addon/components/radio-button.js create mode 100644 app/components/radio-button-input.js create mode 100644 app/components/radio-button.js create mode 100644 tests/integration/components/radio-button-test.js diff --git a/addon/components/radio-button-input.hbs b/addon/components/radio-button-input.hbs new file mode 100644 index 00000000..8ef143f3 --- /dev/null +++ b/addon/components/radio-button-input.hbs @@ -0,0 +1 @@ + diff --git a/addon/components/radio-button-input.js b/addon/components/radio-button-input.js new file mode 100644 index 00000000..56f2fa82 --- /dev/null +++ b/addon/components/radio-button-input.js @@ -0,0 +1,33 @@ +import Component from '@glimmer/component'; +import { once } from '@ember/runloop'; +import { action } from '@ember/object'; + +/** + * The `` itself. + * + * A native replacement for `ember-radio-button`'s component of the same name, kept API- and + * DOM-identical so existing call sites and CSS are unaffected. See `radio-button.js`. + */ +export default class RadioButtonInputComponent extends Component { + /** + * `aria-checked` wants a string, and only when `checked` is genuinely boolean — an undefined + * `@checked` must leave the attribute off rather than render "undefined". + */ + get checkedStr() { + const checked = this.args.checked; + + if (typeof checked === 'boolean') { + return checked.toString(); + } + + return null; + } + + @action change() { + if (this.args.groupValue !== this.args.value) { + // Scheduled rather than called directly: a radio group swaps two inputs in the same + // interaction, and `once` collapses that into a single report of the new value. + once(this.args, 'changed', this.args.value); + } + } +} diff --git a/addon/components/radio-button.hbs b/addon/components/radio-button.hbs new file mode 100644 index 00000000..e0da2ace --- /dev/null +++ b/addon/components/radio-button.hbs @@ -0,0 +1,40 @@ +{{! template-lint-disable no-autofocus-attribute no-positive-tabindex }} +{{! Both rules exist to stop an author HARDCODING these. Here they are pass-through bindings — + the value is whatever the caller supplied, and dropping them would change the component's + argument list. `ember-radio-button`, which this replaces, forwarded both. }} +{{#if (has-block)}} + +{{else}} + +{{/if}} diff --git a/addon/components/radio-button.js b/addon/components/radio-button.js new file mode 100644 index 00000000..5aff5c56 --- /dev/null +++ b/addon/components/radio-button.js @@ -0,0 +1,51 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { isEqual } from '@ember/utils'; + +/** + * A radio button bound to a group value. + * + * This is a native replacement for the `ember-radio-button` addon, which was pinned to a + * prerelease (3.0.0-beta.1) because its last stable release references the removed `Ember` global + * and throws on Ember 5. The component name, arguments and rendered DOM are identical, so call + * sites and stylesheets did not change. + * + * Arguments (all optional except `@value` and `@groupValue`): + * @value this button's value + * @groupValue the currently selected value; the button is checked when the two are equal + * @changed called with this button's value when it becomes selected + * @name @disabled @required @autofocus @tabindex forwarded to the input + * @radioClass class for the input, @radioId its id + * @classNames extra classes for the wrapping label (string or array) + * @checkedClass class added to the label while checked, default "checked" + * @ariaLabelledby @ariaDescribedby + * + * With a block it renders a `