From 9e651b8c8734bfbcabe960cf6cb6965b35441d31 Mon Sep 17 00:00:00 2001 From: Rhodri Jones Date: Mon, 8 Jun 2026 12:30:58 +0100 Subject: [PATCH 01/14] feat(ui): provide/inject disabled + readonly cascade (FE-2810) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Interactive components can now pick up `disabled` and `readonly` state from any ancestor via provide/inject, eliminating the need to wire those props at every call site. Adds `DISABLED_KEY`/`useDisabled`/`provideDisabled` and the same for readonly in `src/utils/`. Each composable returns a resolved `ComputedRef` that ORs the local source with any ancestor-provided value. Either key can be provided at any level (app shell, layout, page, section); the closest ancestor wins. Helper components `` and `` make declarative scoping ergonomic. Wired into 22 disabled-aware components (button, button-group, input, input-password, textarea, number-field, select, select-grouped, select-cards, combobox, autocomplete, checkbox, checkbox-cards, checkbox-group, radio-cards, radio-cards-collapsible, radio-group, switch, toggle, command, search, link) and 3 readonly-aware (input, input-password, textarea). Select-family and number-field readonly deferred — the underlying radix-vue primitives don't expose `readonly`. Existing `disabled`/`readonly` props continue to work standalone; the cascade is OR'd with the prop, so behaviour is unchanged when no provider exists. --- src/index.ts | 10 +++- src/ui/autocomplete/Autocomplete.ce.vue | 6 ++- src/ui/button-group/ButtonGroup.ce.vue | 10 ++-- src/ui/button/Button.ce.vue | 12 +++-- src/ui/checkbox-cards/CheckboxCards.ce.vue | 8 +-- src/ui/checkbox-group/CheckboxGroup.vue | 7 ++- src/ui/checkbox/Checkbox.ce.vue | 10 ++-- src/ui/combobox/Combobox.ce.vue | 6 ++- src/ui/command/Command.vue | 7 ++- src/ui/disabled-provider/DisabledProvider.vue | 18 +++++++ src/ui/disabled-provider/index.ts | 1 + src/ui/input-password/InputPassword.ce.vue | 34 +++++++------ src/ui/input/Input.ce.vue | 36 +++++++------ src/ui/link/Link.ce.vue | 13 +++-- src/ui/number-field/NumberField.ce.vue | 33 ++++++------ .../RadioCardsCollapsible.ce.vue | 12 +++-- src/ui/radio-cards/RadioCards.ce.vue | 10 ++-- src/ui/radio-group/RadioGroup.vue | 7 ++- src/ui/readonly-provider/ReadonlyProvider.vue | 18 +++++++ src/ui/readonly-provider/index.ts | 1 + src/ui/search/Search.ce.vue | 6 ++- src/ui/select-cards/SelectCards.ce.vue | 6 ++- src/ui/select-grouped/SelectGrouped.ce.vue | 8 +-- src/ui/select/Select.ce.vue | 5 +- src/ui/switch/Switch.ce.vue | 10 ++-- src/ui/textarea/Textarea.ce.vue | 12 +++-- src/ui/toggle/Toggle.ce.vue | 10 ++-- src/utils/index.ts | 2 + src/utils/useDisabled.ts | 50 +++++++++++++++++++ src/utils/useReadonly.ts | 50 +++++++++++++++++++ 30 files changed, 318 insertions(+), 100 deletions(-) create mode 100644 src/ui/disabled-provider/DisabledProvider.vue create mode 100644 src/ui/disabled-provider/index.ts create mode 100644 src/ui/readonly-provider/ReadonlyProvider.vue create mode 100644 src/ui/readonly-provider/index.ts create mode 100644 src/utils/useDisabled.ts create mode 100644 src/utils/useReadonly.ts diff --git a/src/index.ts b/src/index.ts index 209b5e6d84..503cf014e2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ export * from "./ui/checkbox-group"; export * from "./ui/combobox"; export * from "./ui/collapsible"; export * from "./ui/dialog"; +export * from "./ui/disabled-provider"; export * from "./ui/drawer"; export * from "./ui/dropdown-menu"; export * from "./ui/description-list"; @@ -43,6 +44,7 @@ export * from "./ui/radio-cards"; export * from "./ui/radio-cards-collapsible"; export * from "./ui/select-grouped"; export * from "./ui/radio-group"; +export * from "./ui/readonly-provider"; export * from "./ui/sanitized"; export * from "./ui/select"; export * from "./ui/select-cards"; @@ -71,5 +73,11 @@ export { isEmptySlot, isMobile, type Theme, - type ITheme + type ITheme, + useDisabled, + useReadonly, + provideDisabled, + provideReadonly, + DISABLED_KEY, + READONLY_KEY } from "./utils"; diff --git a/src/ui/autocomplete/Autocomplete.ce.vue b/src/ui/autocomplete/Autocomplete.ce.vue index e483a08a41..7f8951bb31 100644 --- a/src/ui/autocomplete/Autocomplete.ce.vue +++ b/src/ui/autocomplete/Autocomplete.ce.vue @@ -13,7 +13,7 @@ :uiConfig="props.uiConfig" :popoverClass="props.popoverClass" :type="props.type" - :disabled="props.disabled" + :disabled="isDisabled" :autoFocus="props.autoFocus" :model-value="modelValue" v-model:open="open" @@ -113,7 +113,7 @@ import { ref, computed, watch } from "vue"; import Avatar from "../avatar/Avatar.ce.vue"; import Icon from "../icon/Icon.ce.vue"; import config from "./autocomplete.config"; -import { useStyles } from "../../utils"; +import { useStyles, useDisabled } from "../../utils"; import { cn } from "../../utils"; // --- components // --- utils @@ -164,6 +164,8 @@ const meta = computed(() => ({ const open = ref(false); const processing = ref(false); +const isDisabled = useDisabled(() => props.disabled); + const itemValue = computed((): string => props.itemValue || "value"); const itemLabel = computed((): string => props.itemLabel || "label"); const modelValue = computed((): AutocompleteItemProps | undefined => diff --git a/src/ui/button-group/ButtonGroup.ce.vue b/src/ui/button-group/ButtonGroup.ce.vue index 0de6f89797..2c5e98a7ea 100644 --- a/src/ui/button-group/ButtonGroup.ce.vue +++ b/src/ui/button-group/ButtonGroup.ce.vue @@ -2,7 +2,7 @@