diff --git a/packages/core/src/configDefault.ts b/packages/core/src/configDefault.ts index 3761c314e1..b00d1adc21 100644 --- a/packages/core/src/configDefault.ts +++ b/packages/core/src/configDefault.ts @@ -45,4 +45,11 @@ export const configDefault = { * [text] if asterisks in labels for required fields should be hidden */ hideRequiredAsterisk: false, + + /** + * When false (default), readonly is treated as disabled for backward compatibility. + * When true, readonly and enabled are handled separately and exposed to renderers, + * allowing UI libraries to distinguish between disabled and readonly states. + */ + separateReadonlyFromDisabled: false, }; diff --git a/packages/core/src/mappers/cell.ts b/packages/core/src/mappers/cell.ts index 73f01d7f6a..7934c38cbc 100644 --- a/packages/core/src/mappers/cell.ts +++ b/packages/core/src/mappers/cell.ts @@ -53,7 +53,7 @@ import { JsonFormsCellRendererRegistryEntry, JsonFormsState, } from '../store'; -import { isInherentlyEnabled } from './util'; +import { isInherentlyEnabled, isInherentlyReadonly } from './util'; export interface OwnPropsOfCell extends OwnPropsOfControl { data?: any; @@ -126,10 +126,14 @@ export const mapStateToCellProps = ( * table renderer, determines whether a cell is enabled and should hand * over the prop themselves. If that prop was given, we prefer it over * anything else to save evaluation effort (except for the global readonly - * flag). For example it would be quite expensive to evaluate the same ui schema + * flag when separateReadonlyFromDisabled is disabled). + * For example it would be quite expensive to evaluate the same ui schema * rule again and again for each cell of a table. */ let enabled; - if (state.jsonforms.readonly === true) { + if ( + !config?.separateReadonlyFromDisabled && + state.jsonforms.readonly === true + ) { enabled = false; } else if (typeof ownProps.enabled === 'boolean') { enabled = ownProps.enabled; @@ -144,6 +148,22 @@ export const mapStateToCellProps = ( ); } + /* Similar to enabled, we take a shortcut for readonly state. The parent + * renderer can pass the readonly prop directly if it has already computed it, + * saving re-evaluation for each cell. */ + let readonly; + if (typeof ownProps.readonly === 'boolean') { + readonly = ownProps.readonly; + } else { + readonly = isInherentlyReadonly( + state, + ownProps, + uischema, + schema || rootSchema, + rootData, + config + ); + } const t = getTranslator()(state); const te = getErrorTranslator()(state); const errors = getCombinedErrorMessage( @@ -160,6 +180,7 @@ export const mapStateToCellProps = ( data: Resolve.data(rootData, path), visible, enabled, + readonly, id, path, errors, diff --git a/packages/core/src/mappers/renderer.ts b/packages/core/src/mappers/renderer.ts index e53e4a8d73..d80392c85f 100644 --- a/packages/core/src/mappers/renderer.ts +++ b/packages/core/src/mappers/renderer.ts @@ -83,7 +83,7 @@ import { getUISchemas, getUiSchema, } from '../store'; -import { isInherentlyEnabled } from './util'; +import { isInherentlyEnabled, isInherentlyReadonly } from './util'; import { CombinatorKeyword } from './combinators'; import isEqual from 'lodash/isEqual'; @@ -375,6 +375,10 @@ export interface OwnPropsOfRenderer { * Whether the rendered element should be enabled. */ enabled?: boolean; + /** + * Whether the rendered element should be readonly. + */ + readonly?: boolean; /** * Whether the rendered element should be visible. */ @@ -440,6 +444,12 @@ export interface StatePropsOfRenderer { * Whether the rendered element should be enabled. */ enabled: boolean; + + /** + * Whether the rendered element should be readonly. + */ + readonly?: boolean; + /** * Whether the rendered element should be visible. */ @@ -614,6 +624,14 @@ export const mapStateToControlProps = ( rootData, config ); + const readonly: boolean = isInherentlyReadonly( + state, + ownProps, + uischema, + resolvedSchema || rootSchema, + rootData, + config + ); const schema = resolvedSchema ?? rootSchema; const t = getTranslator()(state); @@ -646,6 +664,7 @@ export const mapStateToControlProps = ( label: i18nLabel, visible, enabled, + readonly, id, path, required, @@ -1062,6 +1081,14 @@ export const mapStateToLayoutProps = ( rootData, config ); + const readonly: boolean = isInherentlyReadonly( + state, + ownProps, + uischema, + undefined, // layouts have no associated schema + rootData, + config + ); // some layouts have labels which might need to be translated const t = getTranslator()(state); @@ -1075,6 +1102,7 @@ export const mapStateToLayoutProps = ( cells: ownProps.cells || getCells(state), visible, enabled, + readonly, path: ownProps.path, data, uischema: ownProps.uischema, diff --git a/packages/core/src/mappers/util.ts b/packages/core/src/mappers/util.ts index c393073d81..bd87bd2ab3 100644 --- a/packages/core/src/mappers/util.ts +++ b/packages/core/src/mappers/util.ts @@ -1,10 +1,10 @@ import { JsonSchema, UISchemaElement } from '../models'; import { JsonFormsState, getAjv } from '../store'; -import { hasEnableRule, isEnabled } from '../util'; +import { hasEnableRule, hasReadonlyRule, isEnabled, isReadonly } from '../util'; /** * Indicates whether the given `uischema` element shall be enabled or disabled. - * Checks the global readonly flag, uischema rule, uischema options (including the config), + * Checks the global readonly flag (unless separateReadonlyFromDisabled is enabled), uischema rule, uischema options (including the config), * the schema and the enablement indicator of the parent. */ export const isInherentlyEnabled = ( @@ -15,29 +15,80 @@ export const isInherentlyEnabled = ( rootData: any, config: any ) => { - if (state?.jsonforms?.readonly) { + if (!config?.separateReadonlyFromDisabled && state?.jsonforms?.readonly) { return false; } if (uischema && hasEnableRule(uischema)) { return isEnabled(uischema, rootData, ownProps?.path, getAjv(state), config); } + if (!config?.separateReadonlyFromDisabled) { + if (typeof uischema?.options?.readonly === 'boolean') { + return !uischema.options.readonly; + } + if (typeof uischema?.options?.readOnly === 'boolean') { + return !uischema.options.readOnly; + } + if (typeof config?.readonly === 'boolean') { + return !config.readonly; + } + if (typeof config?.readOnly === 'boolean') { + return !config.readOnly; + } + if (schema?.readOnly === true) { + return false; + } + } + if (typeof ownProps?.enabled === 'boolean') { + return ownProps.enabled; + } + return true; +}; + +/** + * Indicates whether the given `uischema` element shall be readonly or writable. + * Checks the global readonly flag, uischema rule, uischema options (including the config), + * the schema and the readonly indicator of the parent. + */ +export const isInherentlyReadonly = ( + state: JsonFormsState, + ownProps: any, + uischema: UISchemaElement, + schema: (JsonSchema & { readOnly?: boolean }) | undefined, + rootData: any, + config: any +) => { + if (state?.jsonforms?.readonly) { + return true; + } + + if (uischema && hasReadonlyRule(uischema)) { + return isReadonly( + uischema, + rootData, + ownProps?.path, + getAjv(state), + config + ); + } + if (typeof uischema?.options?.readonly === 'boolean') { - return !uischema.options.readonly; + return uischema.options.readonly; } if (typeof uischema?.options?.readOnly === 'boolean') { - return !uischema.options.readOnly; + return uischema.options.readOnly; } if (typeof config?.readonly === 'boolean') { - return !config.readonly; + return config.readonly; } if (typeof config?.readOnly === 'boolean') { - return !config.readOnly; + return config.readOnly; } if (schema?.readOnly === true) { - return false; + return true; } - if (typeof ownProps?.enabled === 'boolean') { - return ownProps.enabled; + if (typeof ownProps?.readonly === 'boolean') { + return ownProps.readonly; } - return true; + + return false; }; diff --git a/packages/core/src/models/uischema.ts b/packages/core/src/models/uischema.ts index cc5ae938c6..f086da7386 100644 --- a/packages/core/src/models/uischema.ts +++ b/packages/core/src/models/uischema.ts @@ -109,6 +109,15 @@ export enum RuleEffect { * Effect that disables the associated element. */ DISABLE = 'DISABLE', + /** + * Effect that makes the associated element read-only + * (interaction allowed, value cannot be changed). + */ + READONLY = 'READONLY', + /** + * Effect that makes the associated element writable. + */ + WRITABLE = 'WRITABLE', } /** diff --git a/packages/core/src/util/runtime.ts b/packages/core/src/util/runtime.ts index 77caca7621..d4fb4ff10b 100644 --- a/packages/core/src/util/runtime.ts +++ b/packages/core/src/util/runtime.ts @@ -158,6 +158,26 @@ export const evalEnablement = ( } }; +export const evalReadonly = ( + uischema: UISchemaElement, + data: any, + path: string = undefined, + ajv: Ajv, + config: unknown +): boolean => { + const fulfilled = isRuleFulfilled(uischema, data, path, ajv, config); + + switch (uischema.rule.effect) { + case RuleEffect.WRITABLE: + return !fulfilled; + case RuleEffect.READONLY: + return fulfilled; + // writable by default + default: + return false; + } +}; + export const hasShowRule = (uischema: UISchemaElement): boolean => { if ( uischema.rule && @@ -180,6 +200,17 @@ export const hasEnableRule = (uischema: UISchemaElement): boolean => { return false; }; +export const hasReadonlyRule = (uischema: UISchemaElement): boolean => { + if ( + uischema.rule && + (uischema.rule.effect === RuleEffect.READONLY || + uischema.rule.effect === RuleEffect.WRITABLE) + ) { + return true; + } + return false; +}; + export const isVisible = ( uischema: UISchemaElement, data: any, @@ -207,3 +238,17 @@ export const isEnabled = ( return true; }; + +export const isReadonly = ( + uischema: UISchemaElement, + data: any, + path: string = undefined, + ajv: Ajv, + config: unknown +): boolean => { + if (uischema.rule) { + return evalReadonly(uischema, data, path, ajv, config); + } + + return false; +}; diff --git a/packages/core/test/generators/schema.test.ts b/packages/core/test/generators/schema.test.ts index 2171f51a0c..679defefa3 100644 --- a/packages/core/test/generators/schema.test.ts +++ b/packages/core/test/generators/schema.test.ts @@ -113,7 +113,7 @@ test('default schema generation array types', (t) => { test.failing('default schema generation tuple array types', (t) => { const instance: any = { tupleArray: [3.14, 'PI'] }; const schema = generateJsonSchema(instance); - // FIXME: This assumption is the correct one, but we crteate a oneOf in this case + // FIXME: This assumption is the correct one, but we create a oneOf in this case t.deepEqual(schema, { type: 'object', properties: { diff --git a/packages/core/test/util/runtime.test.ts b/packages/core/test/util/runtime.test.ts index 3f068ff658..c72e45f98a 100644 --- a/packages/core/test/util/runtime.test.ts +++ b/packages/core/test/util/runtime.test.ts @@ -27,7 +27,10 @@ import { AndCondition, ControlElement, createAjv, + hasReadonlyRule, isInherentlyEnabled, + isInherentlyReadonly, + isReadonly, JsonFormsCore, LeafCondition, OrCondition, @@ -36,7 +39,11 @@ import { ValidateFunctionCondition, ValidateFunctionContext, } from '../../src'; -import { evalEnablement, evalVisibility } from '../../src/util/runtime'; +import { + evalEnablement, + evalReadonly, + evalVisibility, +} from '../../src/util/runtime'; test('evalVisibility show valid case', (t) => { const leafCondition: LeafCondition = { @@ -775,6 +782,150 @@ test('evalEnablement fail on failWhenUndefined', (t) => { ); }); +test('evalReadonly readonly valid case', (t) => { + const uischema: ControlElement = { + type: 'Control', + scope: '#/properties/value', + rule: { + effect: RuleEffect.READONLY, + condition: { + type: 'LEAF', + scope: '#/properties/ruleValue', + expectedValue: 'bar', + }, + }, + }; + + t.true( + evalReadonly( + uischema, + { value: 'foo', ruleValue: 'bar' }, + undefined, + createAjv(), + undefined + ) + ); +}); + +test('evalReadonly writable valid and invalid cases', (t) => { + const uischema: ControlElement = { + type: 'Control', + scope: '#/properties/value', + rule: { + effect: RuleEffect.WRITABLE, + condition: { + type: 'LEAF', + scope: '#/properties/ruleValue', + expectedValue: 'bar', + }, + }, + }; + + t.false( + evalReadonly( + uischema, + { value: 'foo', ruleValue: 'bar' }, + undefined, + createAjv(), + undefined + ) + ); + t.true( + evalReadonly( + uischema, + { value: 'foo', ruleValue: 'baz' }, + undefined, + createAjv(), + undefined + ) + ); +}); + +test('hasReadonlyRule detects readonly and writable rules', (t) => { + t.true( + hasReadonlyRule({ + type: 'Control', + scope: '#/properties/value', + rule: { + effect: RuleEffect.READONLY, + condition: { + type: 'LEAF', + scope: '#/properties/ruleValue', + expectedValue: 'bar', + }, + }, + }) + ); + t.true( + hasReadonlyRule({ + type: 'Control', + scope: '#/properties/value', + rule: { + effect: RuleEffect.WRITABLE, + condition: { + type: 'LEAF', + scope: '#/properties/ruleValue', + expectedValue: 'bar', + }, + }, + }) + ); + t.false( + hasReadonlyRule({ + type: 'Control', + scope: '#/properties/value', + rule: { + effect: RuleEffect.ENABLE, + condition: { + type: 'LEAF', + scope: '#/properties/ruleValue', + expectedValue: 'bar', + }, + }, + }) + ); +}); + +test('isReadonly defaults to false without rule', (t) => { + t.false( + isReadonly( + { + type: 'Control', + scope: '#/properties/value', + }, + {}, + undefined, + createAjv(), + undefined + ) + ); +}); + +test('isReadonly evaluates readonly rules', (t) => { + const uischema: ControlElement = { + type: 'Control', + scope: '#/properties/value', + rule: { + effect: RuleEffect.READONLY, + condition: { + type: 'LEAF', + scope: '#/properties/ruleValue', + expectedValue: 'bar', + }, + }, + }; + + t.true( + isReadonly( + uischema, + { value: 'foo', ruleValue: 'bar' }, + undefined, + createAjv(), + undefined + ) + ); +}); + test('isInherentlyEnabled disabled globally', (t) => { t.false( isInherentlyEnabled( @@ -788,6 +939,19 @@ test('isInherentlyEnabled disabled globally', (t) => { ); }); +test('isInherentlyEnabled ignores global readonly when separated', (t) => { + t.true( + isInherentlyEnabled( + { jsonforms: { readonly: true } }, + null, + null as any, + undefined, + null, + { separateReadonlyFromDisabled: true } + ) + ); +}); + test('isInherentlyEnabled disabled by ownProps', (t) => { t.false( isInherentlyEnabled( @@ -827,6 +991,19 @@ test('isInherentlyEnabled disabled by uischema', (t) => { ); }); +test('isInherentlyEnabled ignores readonly uischema when separated', (t) => { + t.true( + isInherentlyEnabled( + null as any, + null, + { options: { readonly: true } } as unknown as ControlElement, + undefined, + null, + { separateReadonlyFromDisabled: true } + ) + ); +}); + test('isInherentlyEnabled disabled by uischema over ownProps', (t) => { t.false( isInherentlyEnabled( @@ -892,6 +1069,19 @@ test('isInherentlyEnabled disabled by schema', (t) => { ); }); +test('isInherentlyEnabled ignores readonly schema when separated', (t) => { + t.true( + isInherentlyEnabled( + null as any, + null, + null as any, + { readOnly: true }, + null, + { separateReadonlyFromDisabled: true } + ) + ); +}); + test('isInherentlyEnabled disabled by schema over ownProps', (t) => { t.false( isInherentlyEnabled( @@ -978,6 +1168,15 @@ test('isInherentlyEnabled disabled by config', (t) => { ); }); +test('isInherentlyEnabled ignores readonly config when separated', (t) => { + t.true( + isInherentlyEnabled(null as any, null, null as any, undefined, null, { + readonly: true, + separateReadonlyFromDisabled: true, + }) + ); +}); + test('isInherentlyEnabled enabled by config over ownProps', (t) => { t.true( isInherentlyEnabled( @@ -1038,3 +1237,135 @@ test('isInherentlyEnabled enabled', (t) => { isInherentlyEnabled(null as any, null, null as any, undefined, null, null) ); }); + +test('isInherentlyReadonly readonly globally', (t) => { + t.true( + isInherentlyReadonly( + { jsonforms: { readonly: true } }, + null, + null as any, + undefined, + null, + null + ) + ); +}); + +test('isInherentlyReadonly readonly by uischema option', (t) => { + t.true( + isInherentlyReadonly( + null as any, + { readonly: false }, + { options: { readonly: true } } as unknown as ControlElement, + undefined, + null, + null + ) + ); +}); + +test('isInherentlyReadonly prefer readonly over readOnly', (t) => { + t.false( + isInherentlyReadonly( + null as any, + null, + { + options: { readonly: false, readOnly: true }, + } as unknown as ControlElement, + undefined, + null, + null + ) + ); + t.true( + isInherentlyReadonly( + null as any, + null, + { + options: { readonly: true, readOnly: false }, + } as unknown as ControlElement, + undefined, + null, + null + ) + ); +}); + +test('isInherentlyReadonly readonly by schema', (t) => { + t.true( + isInherentlyReadonly( + null as any, + null, + null as any, + { readOnly: true }, + null, + null + ) + ); +}); + +test('isInherentlyReadonly readonly by ownProps', (t) => { + t.true( + isInherentlyReadonly( + null as any, + { readonly: true }, + null as any, + undefined, + null, + null + ) + ); +}); + +test('isInherentlyReadonly evaluates readonly and writable rules', (t) => { + const readonlyRule: ControlElement = { + type: 'Control', + scope: '#/properties/value', + rule: { + effect: RuleEffect.READONLY, + condition: { + type: 'LEAF', + scope: '#/properties/ruleValue', + expectedValue: 'bar', + }, + }, + }; + const writableRule: ControlElement = { + type: 'Control', + scope: '#/properties/value', + rule: { + effect: RuleEffect.WRITABLE, + condition: { + type: 'LEAF', + scope: '#/properties/ruleValue', + expectedValue: 'bar', + }, + }, + }; + const state = { + jsonforms: { + core: { ajv: createAjv() } as JsonFormsCore, + }, + }; + const data = { + value: 'foo', + ruleValue: 'bar', + }; + + t.true( + isInherentlyReadonly(state, null, readonlyRule, undefined, data, null) + ); + t.false( + isInherentlyReadonly(state, null, writableRule, undefined, data, null) + ); + t.true( + isInherentlyReadonly( + state, + null, + writableRule, + undefined, + { ...data, ruleValue: 'baz' }, + null + ) + ); +}); diff --git a/packages/vue-vuetify/dev/components/ExampleSettings.vue b/packages/vue-vuetify/dev/components/ExampleSettings.vue index bd89d47536..2c8df6fe2d 100644 --- a/packages/vue-vuetify/dev/components/ExampleSettings.vue +++ b/packages/vue-vuetify/dev/components/ExampleSettings.vue @@ -326,6 +326,22 @@ const layouts = appstoreLayouts.map((value: AppstoreLayouts) => ({ + + When false, readonly is treated as disabled for backward + compatibility. When true, readonly and enabled are handled + separately + + + + + + @@ -120,6 +124,7 @@ import { import { DisabledIconFocus } from '../controls'; import { IsDynamicPropertyContext, + isControlEditable, useCombinatorTranslations, useIcons, useJsonForms, @@ -455,6 +460,7 @@ const controlRenderer = defineComponent({ return { ...useCombinatorTranslations(useVuetifyControl(input)), + isControlEditable, nullable, mixedRenderInfos, selectedIndex, diff --git a/packages/vue-vuetify/src/complex/ObjectRenderer.vue b/packages/vue-vuetify/src/complex/ObjectRenderer.vue index 81d0f7f4da..84765e8f64 100644 --- a/packages/vue-vuetify/src/complex/ObjectRenderer.vue +++ b/packages/vue-vuetify/src/complex/ObjectRenderer.vue @@ -3,6 +3,7 @@ @@ -102,6 +104,7 @@ import { } from 'vuetify/components'; import { DisabledIconFocus } from '../controls/directives'; import { + isControlEditable, useCombinatorTranslations, useTranslator, useVuetifyControl, diff --git a/packages/vue-vuetify/src/complex/OneOfTabRenderer.vue b/packages/vue-vuetify/src/complex/OneOfTabRenderer.vue index 33fa515c9b..cd9d884899 100644 --- a/packages/vue-vuetify/src/complex/OneOfTabRenderer.vue +++ b/packages/vue-vuetify/src/complex/OneOfTabRenderer.vue @@ -10,7 +10,7 @@ @@ -90,7 +91,11 @@ import { VWindow, VWindowItem, } from 'vuetify/components'; -import { useCombinatorTranslations, useVuetifyControl } from '../util'; +import { + isControlEditable, + useCombinatorTranslations, + useVuetifyControl, +} from '../util'; import { CombinatorProperties } from './components'; const controlRenderer = defineComponent({ @@ -124,6 +129,7 @@ const controlRenderer = defineComponent({ return { ...useCombinatorTranslations(useVuetifyControl(input)), + isControlEditable, selectedIndex, selectIndex, dialog, diff --git a/packages/vue-vuetify/src/complex/components/AdditionalProperties.vue b/packages/vue-vuetify/src/complex/components/AdditionalProperties.vue index 07ff518692..f97782b9bc 100644 --- a/packages/vue-vuetify/src/complex/components/AdditionalProperties.vue +++ b/packages/vue-vuetify/src/complex/components/AdditionalProperties.vue @@ -20,7 +20,7 @@ :renderers="control.renderers" :cells="control.cells" :config="control.config" - :readonly="!control.enabled" + :readonly="!isControlEditable(control)" :validation-mode="validationMode" :i18n="i18n" :ajv="ajv" @@ -60,6 +60,7 @@ :uischema="element.uischema" :path="element.path" :enabled="control.enabled" + :readonly="control.readonly" :renderers="control.renderers" :cells="control.cells" /> @@ -142,6 +143,7 @@ import { DisabledIconFocus } from '../../controls/directives'; import { useStyles } from '../../styles'; import { useControlAppliedOptions, + isControlEditable, useIcons, useJsonForms, useTranslator, @@ -440,6 +442,7 @@ export default defineComponent({ newPropertyErrors, additionalErrors, icons, + isControlEditable, propertyNameSchema, translations, }; @@ -448,7 +451,7 @@ export default defineComponent({ addPropertyDisabled(): boolean { return ( // add is disabled because the overall control is disabled - !this.control.enabled || + !this.isControlEditable(this.control) || // add is disabled because of contraints (this.appliedOptions.restrict && this.maxPropertiesReached) || // add is disabled because there are errors for the new property name or it is not specified @@ -469,7 +472,7 @@ export default defineComponent({ removePropertyDisabled(): boolean { return ( // add is disabled because the overall control is disabled - !this.control.enabled || + !this.isControlEditable(this.control) || // add is disabled because of contraints (this.appliedOptions.restrict && this.minPropertiesReached) ); diff --git a/packages/vue-vuetify/src/controls/AnyOfStringOrEnumControlRenderer.vue b/packages/vue-vuetify/src/controls/AnyOfStringOrEnumControlRenderer.vue index 8826edae21..93d6c3a609 100644 --- a/packages/vue-vuetify/src/controls/AnyOfStringOrEnumControlRenderer.vue +++ b/packages/vue-vuetify/src/controls/AnyOfStringOrEnumControlRenderer.vue @@ -10,6 +10,7 @@ :id="control.id + '-input'" :class="styles.control.input" :disabled="!control.enabled" + :readonly="control.readonly" :autofocus="appliedOptions.focus" :placeholder="appliedOptions.placeholder" :label="computedLabel" diff --git a/packages/vue-vuetify/src/controls/BooleanControlRenderer.vue b/packages/vue-vuetify/src/controls/BooleanControlRenderer.vue index 2ebf4fe472..7ffff26e7b 100644 --- a/packages/vue-vuetify/src/controls/BooleanControlRenderer.vue +++ b/packages/vue-vuetify/src/controls/BooleanControlRenderer.vue @@ -9,6 +9,7 @@ :id="control.id + '-input'" :class="styles.control.input" :disabled="!control.enabled" + :readonly="control.readonly" :autofocus="appliedOptions.focus" :placeholder="appliedOptions.placeholder" :label="computedLabel" diff --git a/packages/vue-vuetify/src/controls/BooleanToggleControlRenderer.vue b/packages/vue-vuetify/src/controls/BooleanToggleControlRenderer.vue index c64f707693..6d40d85ac9 100644 --- a/packages/vue-vuetify/src/controls/BooleanToggleControlRenderer.vue +++ b/packages/vue-vuetify/src/controls/BooleanToggleControlRenderer.vue @@ -9,6 +9,7 @@ :id="control.id + '-input'" :class="styles.control.input" :disabled="!control.enabled" + :readonly="control.readonly" :autofocus="appliedOptions.focus" :placeholder="appliedOptions.placeholder" :label="computedLabel" diff --git a/packages/vue-vuetify/src/controls/DateControlRenderer.vue b/packages/vue-vuetify/src/controls/DateControlRenderer.vue index 75ac6ae624..4a1e7c5760 100644 --- a/packages/vue-vuetify/src/controls/DateControlRenderer.vue +++ b/packages/vue-vuetify/src/controls/DateControlRenderer.vue @@ -10,6 +10,7 @@ :id="control.id + '-input'" :class="styles.control.input" :disabled="!control.enabled" + :readonly="control.readonly" :autofocus="appliedOptions.focus" :placeholder="appliedOptions.placeholder ?? dateFormat" :label="computedLabel" @@ -35,7 +36,7 @@ min-width="290px" v-bind="vuetifyProps('v-menu')" activator="parent" - :disabled="!control.enabled" + :disabled="!isControlEditable(control)" >