Widen GHeading's icon prop and fold Common/Heading into it

GHeading landed as a copy of Common/Heading with icon narrowed from
IconLike to IconDefinition, which drops Galaxy's own icons (galaxyLogo and
friends) from what the prop accepts. That left two Heading implementations
in the tree -- Common/Heading with 94 importers, GHeading used only by
GModal -- and no way to migrate from one to the other, because the
replacement typed a strictly smaller prop.

Widening icon back to IconLike makes GHeading a superset of
Common/Heading: the two are otherwise identical apart from cosmetic
computed-property style and $brand-secondary vs var(--color-grey-200),
which are the same #dee2e6. Common/Heading becomes a shim like the other
15 components, so the 94 call sites keep working untouched and there is
one Heading again.
This commit is contained in:
mvdbeek
2026-08-26 11:04:38 +02:00
parent d4d596c2ae
commit 2dc61a132c
2 changed files with 6 additions and 186 deletions
@@ -1,9 +1,10 @@
<script setup lang="ts">
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { faAngleDoubleDown, faAngleDoubleUp } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { computed, nextTick, onMounted, onUpdated, ref } from "vue";
import type { IconLike } from "./iconTypes";
import GButton from "./GButton.vue";
interface Props {
@@ -17,7 +18,7 @@ interface Props {
separator?: boolean;
inline?: boolean;
size?: "xs" | "sm" | "md" | "lg" | "xl" | "text";
icon?: IconDefinition;
icon?: IconLike;
truncate?: boolean;
clamp?: number;
collapse?: "open" | "closed" | "none";
+3 -184
View File
@@ -1,186 +1,5 @@
<script setup lang="ts">
import { faAngleDoubleDown, faAngleDoubleUp } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { computed, nextTick, onMounted, onUpdated, ref } from "vue";
<script lang="ts">
import { GHeading } from "@galaxyproject/galaxy-ui";
import type { IconLike } from "@/components/icons/galaxyIcons";
import GButton from "@/components/BaseComponents/GButton.vue";
interface Props {
h1?: boolean;
h2?: boolean;
h3?: boolean;
h4?: boolean;
h5?: boolean;
h6?: boolean;
bold?: boolean;
separator?: boolean;
inline?: boolean;
// TODO: Redo in Vue 3 to use BootstrapSize for prop typing.
size?: "xs" | "sm" | "md" | "lg" | "xl" | "text";
icon?: IconLike;
truncate?: boolean;
clamp?: number;
collapse?: "open" | "closed" | "none";
}
const props = withDefaults(defineProps<Props>(), {
collapse: "none",
icon: undefined,
size: "lg",
});
defineEmits(["click"]);
const sizeClass = computed(() => {
return `h-${props.size}`;
});
const collapsible = computed(() => {
return props.collapse !== "none";
});
const collapsed = computed(() => {
return props.collapse === "closed";
});
const element = computed(() => {
for (const key of ["h1", "h2", "h3", "h4", "h5", "h6"]) {
if (props[key as keyof typeof props]) {
return key;
}
}
return "h1";
});
const headingRef = ref<HTMLElement | null>(null);
const isClamped = ref(false);
function checkClamped() {
if (headingRef.value && props.clamp) {
isClamped.value = headingRef.value.scrollHeight > headingRef.value.clientHeight;
} else {
isClamped.value = false;
}
}
const clampTooltip = computed(() => {
if (isClamped.value) {
return headingRef.value?.textContent?.trim() ?? null;
}
return null;
});
const clampStyle = computed(() => {
if (props.clamp) {
return { webkitLineClamp: props.clamp };
}
return undefined;
});
onMounted(checkClamped);
onUpdated(() => nextTick(checkClamped));
export default GHeading;
</script>
<template>
<div v-if="props.separator" class="separator heading word-wrap-break">
<GButton v-if="collapsible" transparent size="small" icon-only inline @click="$emit('click')">
<FontAwesomeIcon v-if="collapsed" fixed-width :icon="faAngleDoubleDown" />
<FontAwesomeIcon v-else fixed-width :icon="faAngleDoubleUp" />
</GButton>
<div v-else class="stripe"></div>
<component
:is="element"
ref="headingRef"
:title="clampTooltip"
:class="[
sizeClass,
props.bold ? 'font-weight-bold' : '',
collapsible ? 'collapsible' : '',
props.truncate ? 'truncate' : '',
props.clamp ? 'clamp' : '',
]"
:style="clampStyle"
@click="$emit('click')">
<slot />
</component>
<div class="stripe"></div>
</div>
<component
:is="element"
v-else
ref="headingRef"
:title="clampTooltip"
class="heading word-wrap-break"
:class="[
sizeClass,
props.bold ? 'font-weight-bold' : '',
props.inline ? 'inline' : '',
collapsible ? 'collapsible' : '',
props.truncate ? 'truncate' : '',
props.clamp ? 'clamp' : '',
]"
:style="clampStyle"
@click="$emit('click')">
<GButton v-if="collapsible" transparent size="small" icon-only inline>
<FontAwesomeIcon v-if="collapsed" fixed-width :icon="faAngleDoubleDown" />
<FontAwesomeIcon v-else fixed-width :icon="faAngleDoubleUp" />
</GButton>
<FontAwesomeIcon v-if="props.icon" :icon="props.icon" />
<slot />
</component>
</template>
<style lang="scss" scoped>
@import "@/style/scss/theme/blue.scss";
// prettier-ignore
h1, h2, h3, h4, h5, h6 {
&:not(.truncate):not(.clamp) {
display: flex;
}
align-items: center;
gap: 0.4em;
&.inline {
display: inline-flex;
margin-bottom: 0;
}
&.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&.clamp {
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
.collapsible {
cursor: pointer;
}
.separator {
display: grid;
grid-template-columns: 1rem auto 1fr;
gap: 1rem;
align-items: center;
margin-bottom: 0.5rem;
// prettier-ignore
h1, h2, h3, h4, h5, h6 {
margin: 0;
}
.stripe {
display: block;
height: 1px;
background-color: $brand-secondary;
}
}
</style>