Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e2eeaea
test: update test descriptions across dropdown components for consist…
itsJohnnyGrid Oct 29, 2025
80a55a2
feat: add universal UDropdown component
itsJohnnyGrid Oct 29, 2025
983b38c
test: standardize UDropdown test descriptions for improved readabilit…
itsJohnnyGrid Oct 29, 2025
82ee195
refactor: streamline UDropdown props, refactor methods, and add `useD…
itsJohnnyGrid Oct 29, 2025
105f0fd
fix: add `useId` and bind generated ID to UDropdown wrapper element
itsJohnnyGrid Oct 29, 2025
0bdbe3c
refactor: update wrapper config, remove unused props, and clean up st…
itsJohnnyGrid Oct 29, 2025
07077c8
refactor: refactor dropdown components to use UDropdown, useDropdownL…
itsJohnnyGrid Oct 29, 2025
15bdf23
refactor: add `wrapperAttrs` to UDropdownButton, update config, and c…
itsJohnnyGrid Oct 30, 2025
d7f9a6a
refactor: remove unnecessary newline in UCol component script block
itsJohnnyGrid Oct 30, 2025
5390b3a
feat: UCollapsible: introduce new collapsible component
itsJohnnyGrid Oct 30, 2025
697fcd9
refactor: UCollapsible: small fixes
itsJohnnyGrid Oct 31, 2025
6539400
refactor: migrate UDropdown to use UCollapsible, clean up props, and …
itsJohnnyGrid Jan 12, 2026
19ac8bc
refactor: update UCollapsible integration, improve prop handling, and…
itsJohnnyGrid Jan 12, 2026
69fd60f
refactor: update listbox styles and config, adjust height calculation…
itsJohnnyGrid Jan 12, 2026
8d29f23
refactor: update dropdown components and configs to improve consisten…
itsJohnnyGrid Jan 12, 2026
378dfd7
refactor: replace `v-model` with explicit props binding in dropdown c…
itsJohnnyGrid Jan 12, 2026
2395f53
refactor: update dropdown components and configs for improved consist…
itsJohnnyGrid Jan 12, 2026
a3194ed
refactor: adjust listbox styles in form-select config to add spacing …
itsJohnnyGrid Jan 12, 2026
5586135
refactor: remove `useDropdownLabel` composable and inline logic direc…
itsJohnnyGrid Jan 12, 2026
07886f7
refactor: add size variants to toggleIcon configs in dropdown compone…
itsJohnnyGrid Jan 12, 2026
2ff96d7
test: add comprehensive unit tests for dropdown components including …
itsJohnnyGrid Jan 12, 2026
b582fdb
test: update UCollapsible tests to replace `test-wrapper` selectors w…
itsJohnnyGrid Jan 12, 2026
3eab8c0
test: simplify UCollapsible tests by removing unnecessary global moun…
itsJohnnyGrid Jan 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export { default as URow } from "./ui.container-row/URow.vue";
export { default as UGrid } from "./ui.container-grid/UGrid.vue";
export { default as UGroup } from "./ui.container-group/UGroup.vue";
export { default as UGroups } from "./ui.container-groups/UGroups.vue";
export { default as UCollapsible } from "./ui.container-collapsible/UCollapsible.vue";
export { default as UAccordion } from "./ui.container-accordion/UAccordion.vue";
export { default as UAccordionItem } from "./ui.container-accordion-item/UAccordionItem.vue";
export { default as UEmpty } from "./ui.container-empty/UEmpty.vue";
Expand Down
1 change: 1 addition & 0 deletions src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export { default as URow } from "./ui.container-row/URow.vue";
export { default as UGrid } from "./ui.container-grid/UGrid.vue";
export { default as UGroup } from "./ui.container-group/UGroup.vue";
export { default as UGroups } from "./ui.container-groups/UGroups.vue";
export { default as UCollapsible } from "./ui.container-collapsible/UCollapsible.vue";
export { default as UAccordion } from "./ui.container-accordion/UAccordion.vue";
export { default as UAccordionItem } from "./ui.container-accordion-item/UAccordionItem.vue";
export { default as UEmpty } from "./ui.container-empty/UEmpty.vue";
Expand Down
2 changes: 2 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ export const COMPONENTS = {
UToggle: "ui.button-toggle",

/* Dropdowns */
UDropdown: "ui.dropdown",
UDropdownButton: "ui.dropdown-button",
UDropdownBadge: "ui.dropdown-badge",
UDropdownLink: "ui.dropdown-link",
Expand Down Expand Up @@ -293,6 +294,7 @@ export const COMPONENTS = {
UGrid: "ui.container-grid",
UGroup: "ui.container-group",
UGroups: "ui.container-groups",
UCollapsible: "ui.container-collapsible",
UAccordion: "ui.container-accordion",
UAccordionItem: "ui.container-accordion-item",
UEmpty: "ui.container-empty",
Expand Down
1 change: 0 additions & 1 deletion src/ui.container-col/UCol.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ defineExpose({
* Get element / nested component attributes for each config token ✨
* Applies: `class`, `config`, redefined default `props` and dev `vl-...` attributes.
*/

const { getDataTest, wrapperAttrs } = useUI<Config>(defaultConfig);
</script>

Expand Down
190 changes: 190 additions & 0 deletions src/ui.container-collapsible/UCollapsible.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<script setup lang="ts">
import { nextTick, computed, ref, useId, useTemplateRef, watch } from "vue";

import { useUI } from "../composables/useUI";
import { getDefaults } from "../utils/ui";

import vClickOutside from "../v.click-outside/vClickOutside";

import defaultConfig from "./config";
import { COMPONENT_NAME } from "./constants";

import type { Props, Config } from "./types";

defineOptions({ inheritAttrs: false });

const props = withDefaults(defineProps<Props>(), {
...getDefaults<Props, Config>(defaultConfig, COMPONENT_NAME),
});

const emit = defineEmits([
/**
* Triggers when the opened state changes.
* @property {boolean} value
*/
"update:open",

/**
* Triggers when collapsible is opened.
*/
"open",

/**
* Triggers when collapsible is closed.
*/
"close",
]);

const internalIsOpened = ref(false);
const isClickingContent = ref(false);

const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper");
const elementId = props.id || useId();

watch(
() => props.open,
(newValue) => {
if (newValue !== undefined) {
internalIsOpened.value = newValue;
}
},
{ immediate: true },
);

const isOpened = computed({
get: () => internalIsOpened.value,
set: (value) => {
internalIsOpened.value = value;
emit("update:open", value);
},
});

function handleClickOutside() {
if (isClickingContent.value || !props.closeOnOutside) {
return;
}

hide();
}

function toggle() {
if (props.disabled) return;

isOpened.value = !isOpened.value;

isOpened.value ? emit("open") : emit("close");
}

function hide() {
isOpened.value = false;

emit("close");
}

function show() {
if (props.disabled) return;

isOpened.value = true;

emit("open");
}

function onContentClick() {
if (!props.closeOnContent) {
isClickingContent.value = true;

nextTick(() => {
isClickingContent.value = false;
});

return;
}

hide();
}

defineExpose({
/**
* A reference to the component's wrapper element for direct DOM manipulation.
* @property {HTMLDivElement}
*/
wrapperRef,

/**
* Hides the collapsible content.
* @property {function}
*/
hide,

/**
* Shows the collapsible content.
* @property {function}
*/
show,

/**
* Toggles the collapsible visibility.
* @property {function}
*/
toggle,

/**
* Indicates whether the collapsible is opened.
* @property {boolean}
*/
isOpened,
});

/**
* Get element / nested component attributes for each config token ✨
* Applies: `class`, `config`, redefined default `props` and dev `vl-...` attributes.
*/
const mutatedProps = computed(() => ({
/* component state, not a props */
opened: isOpened.value,
}));

const { getDataTest, config, wrapperAttrs, contentAttrs } = useUI<Config>(
defaultConfig,
mutatedProps,
);
</script>

<template>
<div
:id="elementId"
ref="wrapper"
v-click-outside="handleClickOutside"
v-bind="wrapperAttrs"
tabindex="0"
:data-test="getDataTest()"
@click="toggle"
@keydown.enter="toggle"
>
<!--
@slot Use it to add custom trigger element for the collapsible.
@binding {boolean} opened
-->
<slot :opened="isOpened" />

<Transition v-bind="config.contentTransition">
<!--
@slot Use it to add collapsible content.
@binding {boolean} opened
-->
<div
v-if="isOpened"
v-bind="contentAttrs"
:data-test="getDataTest('content')"
@click.stop="onContentClick"
>
<!--
@slot Use it to add some content need to be shown.
@binding {boolean} opened
@binding {string} contentClasses
-->
<slot name="content" :opened="isOpened" :content-classes="contentAttrs.class" />
</div>
</Transition>
</div>
</template>
45 changes: 45 additions & 0 deletions src/ui.container-collapsible/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export default /*tw*/ {
wrapper: {
base: `
relative inline-block h-max rounded-medium transition cursor-pointer outline-transparent
focus-visible:outline-medium focus-visible:outline-offset-2 focus-visible:outline-primary`,
variants: {
disabled: {
true: "cursor-not-allowed",
},
},
},
content: {
base: "z-10 w-fit",
variants: {
absolute: {
true: "absolute",
false: "",
},
yPosition: {
top: "bottom-full mb-1",
bottom: "top-full mt-1",
},
xPosition: {
left: "left-0",
right: "right-0",
},
},
},
contentTransition: {
enterActiveClass: "ease-out duration-200",
enterFromClass: "opacity-0 scale-95",
enterToClass: "opacity-100 scale-100",
leaveActiveClass: "ease-in duration-150",
leaveFromClass: "opacity-100 scale-100",
leaveToClass: "opacity-0 scale-95",
},
defaults: {
absolute: true,
yPosition: "bottom",
xPosition: "left",
closeOnOutside: true,
closeOnContent: false,
disabled: false,
},
};
1 change: 1 addition & 0 deletions src/ui.container-collapsible/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const COMPONENT_NAME = "UCollapsible";
17 changes: 17 additions & 0 deletions src/ui.container-collapsible/storybook/docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Meta, Title, Subtitle, Description, Primary, Controls, Stories, Source } from "@storybook/addon-docs/blocks";
import { getSource } from "../../utils/storybook.ts";

import * as stories from "./stories.ts";
import defaultConfig from "../config.ts?raw"

<Meta of={stories} />
<Title of={stories} />
<Subtitle of={stories} />
<Description of={stories} />
<Primary of={stories} />
<Controls of={stories.Default} />
<Stories of={stories} />

## Default config
<Source code={getSource(defaultConfig)} language="jsx" dark />

Loading