UNPKG

@fastkit/vue-disabled-reason

Version:

A headless Vue utility that monitors disabled elements and provides a slot or render function to display the reason.

311 lines (240 loc) • 7.97 kB
# @fastkit/vue-disabled-reason 🌐 English | [ę—„ęœ¬čŖž](https://github.com/dadajam4/fastkit/blob/main/packages/vue-disabled-reason/README-ja.md) A library providing utility functions to define headless Vue.js components for monitoring disabled elements and displaying reasons. ## Features - **Headless Design**: Create components that handle only logic without providing UI - **Automatic Disabled State Detection**: Automatically monitors `disabled` and `aria-disabled` attributes - **Flexible Display Control**: Display reasons through slots or render functions - **Full TypeScript Support**: Type safety with strict type definitions - **Advanced Customization**: Support for custom VNode traversal handling - **Performance Optimization**: Efficient element monitoring and rendering control ## Installation ```bash npm install @fastkit/vue-disabled-reason # or pnpm add @fastkit/vue-disabled-reason ``` ### Required CSS Import Before using the library, make sure to import the CSS file. This CSS sets `position: relative` on monitored elements, allowing the library to properly insert reason display elements. ```typescript // Import at the main entry point first import '@fastkit/vue-disabled-reason/vue-disabled-reason.css'; ``` Or import in HTML: ```html <link rel="stylesheet" href="@fastkit/vue-disabled-reason/vue-disabled-reason.css"> ``` ## Basic Usage ### Simple Disabled Reason Display ```vue <template> <div> <!-- Wrap disabled button --> <MyDisabledReason> <button :disabled="hasNoPermission"> Execute Delete </button> <!-- Display disabled reason via slot --> <template #reason> Insufficient permissions </template> </MyDisabledReason> </div> </template> <script setup lang="ts"> import { ref } from 'vue'; import { MyDisabledReason } from './components/MyDisabledReason.vue'; const hasNoPermission = ref(true); </script> ``` ### Reason Display with reason Property You can also use the `reason` property instead of slots. ```vue <template> <div> <!-- Specify reason with reason property --> <MyDisabledReason reason="Administrator privileges required"> <button :disabled="!isAdmin"> System Settings </button> </MyDisabledReason> <!-- Dynamic reason setting --> <MyDisabledReason :reason="disableReason"> <button :disabled="isDisabled"> Delete Data </button> </MyDisabledReason> </div> </template> <script setup lang="ts"> import { computed } from 'vue'; const isAdmin = ref(false); const hasDeletePermission = ref(false); const disableReason = computed(() => hasDeletePermission.value ? null : 'Delete permission required' ); </script> ``` ### Custom Component Definition ```typescript import { defineDisabledReasonComponent } from '@fastkit/vue-disabled-reason'; import { VTooltip } from './components/VTooltip.vue'; export const MyDisabledReason = defineDisabledReasonComponent({ name: 'MyDisabledReason', setup(api) { return (reason) => ( <VTooltip disabled={!reason || !api.disabled} v-slots={{ activator: ({ attrs }) => <span {...attrs} />, default: () => reason, }} /> ); }, }); ``` ## Advanced Usage ### Custom Container Specification By default, reason display elements are inserted into the root element of the monitored target. You can customize the insertion position using `DISABLED_REASON_CONTAINER_BIND`. ```vue <template> <MyDisabledReason> <div class="button-container"> <!-- Specify insertion position with DISABLED_REASON_CONTAINER_BIND --> <div v-bind="DISABLED_REASON_CONTAINER_BIND"> <button :disabled="isDisabled">Change Settings</button> </div> <span>System Settings</span> </div> <template #reason> No permission to change system settings </template> </MyDisabledReason> </template> <script setup lang="ts"> import { DISABLED_REASON_CONTAINER_BIND } from '@fastkit/vue-disabled-reason'; const isDisabled = ref(true); </script> ``` ### Individual Handling for Multiple Buttons When there are multiple disableable elements, each must be wrapped with individual components. ```vue <template> <div> <h3>Data Management</h3> <div> <!-- Edit button --> <MyDisabledReason> <div v-bind="DISABLED_REASON_CONTAINER_BIND"> <button :disabled="!canEdit">Edit</button> </div> <template #reason> No edit permission </template> </MyDisabledReason> <!-- Delete button --> <MyDisabledReason> <div v-bind="DISABLED_REASON_CONTAINER_BIND"> <button :disabled="!canDelete">Delete</button> </div> <template #reason> No delete permission </template> </MyDisabledReason> </div> </div> </template> <script setup lang="ts"> import { computed } from 'vue'; import { DISABLED_REASON_CONTAINER_BIND } from '@fastkit/vue-disabled-reason'; const userRole = ref('viewer'); // 'admin', 'editor', 'viewer' const canEdit = computed(() => ['admin', 'editor'].includes(userRole.value)); const canDelete = computed(() => userRole.value === 'admin'); </script> ``` ## API Reference ### defineDisabledReasonComponent Defines a headless disabled reason component. ```typescript function defineDisabledReasonComponent<Props>( options: DisabledReasonComponentOptions<Props> ): DefineComponent ``` **Options:** - `name?`: Component name - `props?`: Additional property definitions - `skipVNode?`: VNode traversal skip handler - `setup`: Setup function that returns a render function ### DisabledReasonAPI API available within components. ```typescript interface DisabledReasonAPI<Props = {}> { readonly props: ExtractPropTypes<Props>; readonly disabled: boolean; } ``` ### DISABLED_REASON_CONTAINER_BIND Bind object for customizing the insertion position of disabled reason display elements. ```typescript const DISABLED_REASON_CONTAINER_BIND: { 'data-disabled-reason-container': ''; } ``` **Usage:** ```vue <template> <MyDisabledReason> <!-- Disabled reason will be inserted within this element --> <div v-bind="DISABLED_REASON_CONTAINER_BIND"> <button :disabled="isDisabled">Button</button> </div> <template #reason> Reason text for being disabled </template> </MyDisabledReason> </template> ``` **Notes:** - Only the first disableable element found within the specified container will be monitored - When there are multiple disableable elements, wrap each with individual components ## Monitored Elements ### Standard Disableable Elements - `<button>` - `<input>` (all types) - `<textarea>` - `<select>` - `<fieldset>` - `<optgroup>` - `<option>` ### ARIA-Compatible Elements Elements with the following roles that have `aria-disabled` attribute (including `<a>` tags): - `button` - `link` - `menuitem` - `menuitemcheckbox` - `menuitemradio` - `option` - `radio` - `slider` - `spinbutton` - `switch` - `tab` - `checkbox` - `gridcell` - `textbox` ## Performance Considerations - **Efficient Element Search**: Minimal DOM traversal - **Rendering Optimization**: Re-render only when disabled state changes - **Memory Management**: Proper cleanup processing - **Event Optimization**: Register event listeners only when necessary ## Limitations - **Single Element Monitoring**: One component instance monitors only the first disableable element found. For multiple elements, wrap each with individual components - **Dynamic Elements**: Monitoring dynamically added elements requires remounting - **CSS Pseudo-classes**: Cannot detect disabled states set by CSS pseudo-classes (`:disabled`, etc.) - **JavaScript Dynamic Changes**: Dynamic disabled state changes via JavaScript are reflected after `nextTick` ## Related Packages - `@fastkit/vue-utils` - Vue.js utilities (dependency) - `vue` - Vue.js framework (peer dependency) ## License MIT