diff --git a/.changeset/fluffy-cameras-drum.md b/.changeset/fluffy-cameras-drum.md
new file mode 100644
index 00000000..03696851
--- /dev/null
+++ b/.changeset/fluffy-cameras-drum.md
@@ -0,0 +1,6 @@
+---
+'@crowdstrike/ember-toucan-core': patch
+'@crowdstrike/ember-toucan-form': patch
+---
+
+Add a lock icon to readonly and disabled states for all form components.
diff --git a/docs/components/checkbox-field/index.md b/docs/components/checkbox-field/index.md
index 20eed1c0..e0e6cb38 100644
--- a/docs/components/checkbox-field/index.md
+++ b/docs/components/checkbox-field/index.md
@@ -204,7 +204,7 @@ Target the error block via `data-error`.
- <:label>Label
+ <:label>Label
<:hint>Select link
diff --git a/docs/components/checkbox-group-field/index.md b/docs/components/checkbox-group-field/index.md
index 1f232588..02c24487 100644
--- a/docs/components/checkbox-group-field/index.md
+++ b/docs/components/checkbox-group-field/index.md
@@ -237,7 +237,7 @@ Consumers have direct access to the underlying [checkbox element](https://develo
- <:label>Label
+ <:label>Label
<:hint>Select an option link
diff --git a/docs/components/file-input-field/index.md b/docs/components/file-input-field/index.md
index 8d4eb041..5fff82fb 100644
--- a/docs/components/file-input-field/index.md
+++ b/docs/components/file-input-field/index.md
@@ -250,7 +250,7 @@ Target the trash icon button via `data-delete-file`.
@deleteLabel='Delete file'
@trigger='Browse Files'
>
- <:label>Label
+ <:label>Label
<:hint>Hint text link
diff --git a/docs/components/input-field/index.md b/docs/components/input-field/index.md
index 0833a4e2..17734506 100644
--- a/docs/components/input-field/index.md
+++ b/docs/components/input-field/index.md
@@ -177,7 +177,7 @@ Target the error block via `data-error`.
- <:label>Label
+ <:label>Label
<:hint>Hint text link
diff --git a/docs/components/radio-field/index.md b/docs/components/radio-field/index.md
index f5908192..f758c31d 100644
--- a/docs/components/radio-field/index.md
+++ b/docs/components/radio-field/index.md
@@ -186,7 +186,7 @@ Target the hint block via `data-hint`.
@name='options-b'
@value='option-1'
>
- <:label>Label
+ <:label>Label
<:hint>Hint text link
diff --git a/docs/components/radio-group-field/index.md b/docs/components/radio-group-field/index.md
index 8e27c86b..f0d2fe23 100644
--- a/docs/components/radio-group-field/index.md
+++ b/docs/components/radio-group-field/index.md
@@ -245,7 +245,7 @@ Consumers have direct access to the underlying [radio element](https://developer
- <:label>Label
+ <:label>Label
<:hint>Select an option link
diff --git a/docs/components/textarea-field/index.md b/docs/components/textarea-field/index.md
index ef1598b6..c560c8a1 100644
--- a/docs/components/textarea-field/index.md
+++ b/docs/components/textarea-field/index.md
@@ -198,7 +198,7 @@ Target the error block via `data-error`.
- <:label>Label
+ <:label>Label
<:hint>Select an option link
@@ -242,7 +242,7 @@ Target the error block via `data-error`.
/>
-### TextareaField with character count
+### TextareaField with character count
-### TextareaField with character count with a single error
+### TextareaField with character count with a single error
-### TextareaField with character count with multiple errors
+### TextareaField with character count with multiple errors
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/ember-toucan-core/src/-private/components/lock-icon.ts b/packages/ember-toucan-core/src/-private/components/lock-icon.ts
new file mode 100644
index 00000000..e5789940
--- /dev/null
+++ b/packages/ember-toucan-core/src/-private/components/lock-icon.ts
@@ -0,0 +1,11 @@
+import templateOnlyComponent from '@ember/component/template-only';
+
+export interface ToucanLockIconComponentSignature {
+ Element: SVGElement;
+ Args: {};
+ Blocks: {
+ default: [];
+ };
+}
+
+export default templateOnlyComponent();
diff --git a/packages/ember-toucan-core/src/components/form/fields/checkbox-group.hbs b/packages/ember-toucan-core/src/components/form/fields/checkbox-group.hbs
index a9fc0894..99da4d42 100644
--- a/packages/ember-toucan-core/src/components/form/fields/checkbox-group.hbs
+++ b/packages/ember-toucan-core/src/components/form/fields/checkbox-group.hbs
@@ -15,12 +15,19 @@
)
)
}}
-
+
{{#if (has-block "label")}}
{{yield to="label"}}
{{else}}
{{@label}}
{{/if}}
+
+ {{#if this.isReadOnlyOrDisabled}}
+
+ {{/if}}
{{/if}}
@@ -52,6 +59,7 @@
isDisabled=@isDisabled
isReadOnly=@isReadOnly
selectedValues=@value
+ isGrouped=true
)
)
}}
diff --git a/packages/ember-toucan-core/src/components/form/fields/checkbox-group.ts b/packages/ember-toucan-core/src/components/form/fields/checkbox-group.ts
index 17d77ad8..8f2f28f9 100644
--- a/packages/ember-toucan-core/src/components/form/fields/checkbox-group.ts
+++ b/packages/ember-toucan-core/src/components/form/fields/checkbox-group.ts
@@ -2,6 +2,7 @@ import Component from '@glimmer/component';
import { action } from '@ember/object';
import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists';
+import LockIcon from '../../../-private/components/lock-icon';
import CheckboxFieldComponent from './checkbox';
import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists';
@@ -74,6 +75,7 @@ export interface ToucanFormCheckboxGroupFieldComponentSignature {
export default class ToucanFormCheckboxGroupFieldComponent extends Component {
CheckboxFieldComponent = CheckboxFieldComponent;
+ LockIcon = LockIcon;
assertBlockOrArgumentExists = ({
blockExists,
@@ -83,6 +85,10 @@ export default class ToucanFormCheckboxGroupFieldComponent extends Component
assertBlockOrArgumentExists({ blockExists, argName, arg, isRequired });
+ get isReadOnlyOrDisabled() {
+ return this.args?.isDisabled || this.args?.isReadOnly;
+ }
+
@action
handleInput(_: boolean, e: Event | InputEvent): void {
let value = this.args.value ? [...this.args.value] : [];
diff --git a/packages/ember-toucan-core/src/components/form/fields/checkbox.hbs b/packages/ember-toucan-core/src/components/form/fields/checkbox.hbs
index eea0746d..255c50dd 100644
--- a/packages/ember-toucan-core/src/components/form/fields/checkbox.hbs
+++ b/packages/ember-toucan-core/src/components/form/fields/checkbox.hbs
@@ -37,7 +37,7 @@
)
}}
@@ -46,6 +46,10 @@
{{else}}
{{@label}}
{{/if}}
+
+ {{#if this.isDisabledOrDisabledAndNotInAGroup}}
+
+ {{/if}}
{{/if}}
diff --git a/packages/ember-toucan-core/src/components/form/fields/checkbox.ts b/packages/ember-toucan-core/src/components/form/fields/checkbox.ts
index e4487adb..59385374 100644
--- a/packages/ember-toucan-core/src/components/form/fields/checkbox.ts
+++ b/packages/ember-toucan-core/src/components/form/fields/checkbox.ts
@@ -2,6 +2,7 @@ import Component from '@glimmer/component';
import { assert } from '@ember/debug';
import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists';
+import LockIcon from '../../../-private/components/lock-icon';
import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists';
import type { ErrorMessage } from '../../../-private/types';
@@ -42,6 +43,14 @@ export interface ToucanFormCheckboxFieldComponentSignature {
*/
isIndeterminate?: ToucanFormCheckboxControlComponentSignature['Args']['isIndeterminate'];
+ /**
+ * Helps us determine if we are in a checkbox-group or not. This should only be applied internally
+ * when using CheckboxGroup.
+ *
+ * @internal
+ */
+ isGrouped?: boolean;
+
/**
* Provide a string to this argument to render inside of the label tag.
*/
@@ -88,6 +97,8 @@ export interface ToucanFormCheckboxFieldComponentSignature {
}
export default class ToucanFormCheckboxFieldComponent extends Component {
+ LockIcon = LockIcon;
+
assertBlockOrArgumentExists = ({
blockExists,
argName,
@@ -119,4 +130,19 @@ export default class ToucanFormCheckboxFieldComponent extends Component
+
{{#if (has-block "label")}}
{{yield to="label"}}
{{else}}
{{@label}}
{{/if}}
+
+ {{#if this.isReadOnlyOrDisabled}}
+
+ {{/if}}
{{/if}}
diff --git a/packages/ember-toucan-core/src/components/form/fields/file-input.ts b/packages/ember-toucan-core/src/components/form/fields/file-input.ts
index bafc0a4c..a011d4d1 100644
--- a/packages/ember-toucan-core/src/components/form/fields/file-input.ts
+++ b/packages/ember-toucan-core/src/components/form/fields/file-input.ts
@@ -3,6 +3,7 @@ import { assert } from '@ember/debug';
import { action } from '@ember/object';
import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists';
+import LockIcon from '../../../-private/components/lock-icon';
import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists';
import type { ErrorMessage } from '../../../-private/types';
@@ -81,6 +82,8 @@ export interface ToucanFormFileInputFieldComponentSignature {
}
export default class ToucanFormFileInputFieldComponent extends Component {
+ LockIcon = LockIcon;
+
assertBlockOrArgumentExists = ({
blockExists,
argName,
@@ -110,6 +113,10 @@ export default class ToucanFormFileInputFieldComponent extends Component
+
{{#if (has-block "label")}}
{{yield to="label"}}
{{else}}
{{@label}}
{{/if}}
+
+ {{#if this.isReadOnlyOrDisabled}}
+
+ {{/if}}
{{/if}}
diff --git a/packages/ember-toucan-core/src/components/form/fields/input.ts b/packages/ember-toucan-core/src/components/form/fields/input.ts
index 41a7b3cc..334f2179 100644
--- a/packages/ember-toucan-core/src/components/form/fields/input.ts
+++ b/packages/ember-toucan-core/src/components/form/fields/input.ts
@@ -4,6 +4,7 @@ import { assert } from '@ember/debug';
import { action } from '@ember/object';
import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists';
+import LockIcon from '../../../-private/components/lock-icon';
import CharacterCount from '../../../components/form/controls/character-count';
import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists';
@@ -69,6 +70,7 @@ export default class ToucanFormInputFieldComponent extends Component
assertBlockOrArgumentExists({ blockExists, argName, arg, isRequired });
+ get hasError() {
+ return Boolean(this.args?.error);
+ }
+
+ get isReadOnlyOrDisabled() {
+ return this.args?.isDisabled || this.args?.isReadOnly;
+ }
+
@action
handleCount(event: Event | InputEvent): void {
assert(
@@ -87,8 +97,4 @@ export default class ToucanFormInputFieldComponent extends Component
+
{{#if (has-block "label")}}
{{yield to="label"}}
{{else}}
{{@label}}
{{/if}}
+
+ {{#if this.isReadOnlyOrDisabled}}
+
+ {{/if}}
{{/if}}
diff --git a/packages/ember-toucan-core/src/components/form/fields/radio-group.ts b/packages/ember-toucan-core/src/components/form/fields/radio-group.ts
index 42ea9674..829d7a07 100644
--- a/packages/ember-toucan-core/src/components/form/fields/radio-group.ts
+++ b/packages/ember-toucan-core/src/components/form/fields/radio-group.ts
@@ -2,6 +2,7 @@ import Component from '@glimmer/component';
import { action } from '@ember/object';
import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists';
+import LockIcon from '../../../-private/components/lock-icon';
import RadioFieldComponent from './radio';
import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists';
@@ -75,6 +76,7 @@ export interface ToucanFormRadioGroupFieldComponentSignature {
export default class ToucanFormRadioGroupFieldComponent extends Component {
RadioFieldComponent = RadioFieldComponent;
+ LockIcon = LockIcon;
assertBlockOrArgumentExists = ({
blockExists,
@@ -84,6 +86,10 @@ export default class ToucanFormRadioGroupFieldComponent extends Component
assertBlockOrArgumentExists({ blockExists, argName, arg, isRequired });
+ get isReadOnlyOrDisabled() {
+ return this.args?.isDisabled || this.args?.isReadOnly;
+ }
+
@action
handleInput(value: string, e: Event | InputEvent): void {
this.args.onChange?.(value, e);
diff --git a/packages/ember-toucan-core/src/components/form/fields/textarea.hbs b/packages/ember-toucan-core/src/components/form/fields/textarea.hbs
index 25a3cdbc..30976599 100644
--- a/packages/ember-toucan-core/src/components/form/fields/textarea.hbs
+++ b/packages/ember-toucan-core/src/components/form/fields/textarea.hbs
@@ -13,12 +13,20 @@
)
)
}}
-
+
{{#if (has-block "label")}}
{{yield to="label"}}
{{else}}
{{@label}}
{{/if}}
+
+ {{#if this.isReadOnlyOrDisabled}}
+
+ {{/if}}
{{/if}}
diff --git a/packages/ember-toucan-core/src/components/form/fields/textarea.ts b/packages/ember-toucan-core/src/components/form/fields/textarea.ts
index 49474584..6f91eebd 100644
--- a/packages/ember-toucan-core/src/components/form/fields/textarea.ts
+++ b/packages/ember-toucan-core/src/components/form/fields/textarea.ts
@@ -4,6 +4,7 @@ import { assert } from '@ember/debug';
import { action } from '@ember/object';
import assertBlockOrArgumentExists from '../../../-private/assert-block-or-argument-exists';
+import LockIcon from '../../../-private/components/lock-icon';
import CharacterCount from '../../../components/form/controls/character-count';
import type { AssertBlockOrArg } from '../../../-private/assert-block-or-argument-exists';
@@ -69,6 +70,8 @@ export default class ToucanFormTextareaFieldComponent extends Component [data-control]')
.hasNoClass('shadow-error-outline');
+
+ assert.dom('[data-lock-icon]').doesNotExist();
});
test('it renders with a hint', async function (assert) {
@@ -112,20 +114,24 @@ module('Integration | Component | Fields | CheckboxField', function (hooks) {
);
});
- test('it disables the checkbox using `@isDisabled`', async function (assert) {
+ test('it disables the checkbox using `@isDisabled` and renders a lock icon', async function (assert) {
await render(
);
assert.dom('[data-checkbox]').isDisabled();
+
+ assert.dom('[data-lock-icon]').exists();
});
- test('it sets readonly on the checkbox using `@isReadOnly`', async function (assert) {
+ test('it sets readonly on the checkbox using `@isReadOnly` and renders a lock icon', async function (assert) {
await render(
);
assert.dom('[data-checkbox]').hasAttribute('readonly');
+
+ assert.dom('[data-lock-icon]').exists();
});
test('it spreads attributes to the underlying checkbox', async function (assert) {
diff --git a/test-app/tests/integration/components/checkbox-group-field-test.gts b/test-app/tests/integration/components/checkbox-group-field-test.gts
index d65534fc..4b1d6e85 100644
--- a/test-app/tests/integration/components/checkbox-group-field-test.gts
+++ b/test-app/tests/integration/components/checkbox-group-field-test.gts
@@ -18,6 +18,8 @@ module('Integration | Component | Fields | CheckboxGroup', function (hooks) {
assert.dom('[data-group-field]').hasNoAttribute('aria-invalid');
assert.dom('[data-label]').hasText('Label');
+
+ assert.dom('[data-lock-icon]').doesNotExist();
});
test('it renders yielded CheckboxFields', async function (assert) {
@@ -151,7 +153,7 @@ module('Integration | Component | Fields | CheckboxGroup', function (hooks) {
assert.dom('[data-checkbox-2]').isChecked();
});
- test('it disables the fieldset and all child checkboxes using `@isDisabled` at the root', async function (assert) {
+ test('it disables the fieldset and all child checkboxes using `@isDisabled` at the root and renders a lock icon', async function (assert) {
await render(
);
assert.dom('[data-file-input-field]').hasAttribute('readonly');
+
+ assert.dom('[data-lock-icon]').exists();
});
test('it spreads attributes to the underlying file-input-field', async function (assert) {
diff --git a/test-app/tests/integration/components/input-field-test.gts b/test-app/tests/integration/components/input-field-test.gts
index 7bc73862..03130de4 100644
--- a/test-app/tests/integration/components/input-field-test.gts
+++ b/test-app/tests/integration/components/input-field-test.gts
@@ -25,6 +25,8 @@ module('Integration | Component | Fields | Input', function (hooks) {
assert.dom(input).hasAttribute('type', 'text');
assert.dom(input).hasAttribute('id');
assert.dom(input).hasNoClass('shadow-error-outline');
+
+ assert.dom('[data-lock-icon]').doesNotExist();
});
test('it throws an assertion error if no `@label` or `:label` is provided', async function (assert) {
@@ -216,19 +218,23 @@ module('Integration | Component | Fields | Input', function (hooks) {
assert.dom('[data-character]').hasText('11 / 255');
});
- test('it disables the input using `@isDisabled`', async function (assert) {
+ test('it disables the input using `@isDisabled` and renders a lock icon', async function (assert) {
await render(
);
assert.dom('[data-input]').isDisabled();
+
+ assert.dom('[data-lock-icon]').exists();
});
- test('it sets readonly on the input using `@isReadOnly`', async function (assert) {
+ test('it sets readonly on the input using `@isReadOnly` and renders a lock icon', async function (assert) {
await render(
);
assert.dom('[data-input]').hasAttribute('readonly');
+
+ assert.dom('[data-lock-icon]').exists();
});
});
diff --git a/test-app/tests/integration/components/radio-group-field-test.gts b/test-app/tests/integration/components/radio-group-field-test.gts
index 67004434..8f47d827 100644
--- a/test-app/tests/integration/components/radio-group-field-test.gts
+++ b/test-app/tests/integration/components/radio-group-field-test.gts
@@ -18,6 +18,8 @@ module('Integration | Component | Fields | RadioGroup', function (hooks) {
assert.dom('[data-group-field]').hasAttribute('aria-required');
assert.dom('[data-label]').hasText('Label');
+
+ assert.dom('[data-lock-icon]').doesNotExist();
});
test('it sets "role" by default', async function (assert) {
@@ -98,7 +100,7 @@ module('Integration | Component | Fields | RadioGroup', function (hooks) {
assert.dom('[data-radio-2]').isChecked();
});
- test('it disables the fieldset and all child radios using `@isDisabled` at the root', async function (assert) {
+ test('it disables the fieldset and all child radios using `@isDisabled` at the root and renders a lock icon', async function (assert) {
await render(
);
assert.dom('[data-textarea]').isDisabled();
assert.dom('[data-textarea]').hasClass('text-disabled');
+
+ assert.dom('[data-lock-icon]').exists();
});
- test('it sets readonly on the textarea using `@isReadOnly`', async function (assert) {
+ test('it sets readonly on the textarea using `@isReadOnly` and renders a lock icon', async function (assert) {
await render(
);
assert.dom('[data-textarea]').hasAttribute('readonly');
+
+ assert.dom('[data-lock-icon]').exists();
});
test('it spreads attributes to the underlying textarea', async function (assert) {