feat(ui): if a link toggles a query flag, then the "active" colors reflect whether any query is currently set
This commit is contained in:
parent
4ea287c17f
commit
e09d0a20fa
|
@ -1,11 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, useSlots } from 'vue'
|
||||
|
||||
import { type RouterLinkProps } from 'vue-router'
|
||||
import { type RouterLinkProps, RouterLink, useLink } from 'vue-router'
|
||||
import { type ColorProps, type DefaultProps, type VariantProps, color, isNoColors } from '~/composables/color';
|
||||
import { type WidthProps, width } from '~/composables/width'
|
||||
import { type AlignmentProps, align } from '~/composables/alignment'
|
||||
|
||||
import { fromProps, notUndefined } from '~/ui/composables/useModal.ts'
|
||||
|
||||
const props = defineProps<{
|
||||
thickWhenActive?: true
|
||||
|
||||
|
@ -26,11 +28,22 @@ const isExternalLink = computed(() =>
|
|||
typeof props.to === 'string' && (props.to.startsWith('http') || props.to.startsWith('./'))
|
||||
)
|
||||
|
||||
/**
|
||||
* Any query matches
|
||||
*/
|
||||
const isNoMatchingQueryFlags = computed(() =>
|
||||
fromProps(props, notUndefined)?.every(({ isOpen }) => !isOpen.value)
|
||||
)
|
||||
|
||||
const [fontWeight, activeFontWeight] = 'solid' in props || props.thickWhenActive ? [600, 900] : [400, 400]
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const isIconOnly = computed(() => !!props.icon && (!slots.default || 'square' in props && props.square || 'squareSmall' in props && props.squareSmall ))
|
||||
const isIconOnly = computed(() =>
|
||||
!!props.icon && (
|
||||
!useSlots().default
|
||||
|| 'square' in props && props.square
|
||||
|| 'squareSmall' in props && props.squareSmall
|
||||
)
|
||||
)
|
||||
|
||||
const button = ref()
|
||||
|
||||
|
@ -40,7 +53,7 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="isExternalLink ? 'a' : 'RouterLink'"
|
||||
<component :is="isExternalLink ? 'a' : RouterLink"
|
||||
v-bind="
|
||||
color(props, ['interactive'])(
|
||||
width(props,
|
||||
|
@ -56,6 +69,7 @@ onMounted(() => {
|
|||
isIconOnly && $style['is-icon-only'],
|
||||
(isNoColors(props) || props.forceUnderline) && $style['force-underline'],
|
||||
isNoColors(props) && $style['no-spacing'],
|
||||
isNoMatchingQueryFlags && 'router-link-no-matching-query-flag'
|
||||
]"
|
||||
:href="isExternalLink ? to.toString() : undefined"
|
||||
:to="isExternalLink ? undefined : to"
|
||||
|
|
|
@ -566,7 +566,7 @@
|
|||
background-color: var(--pressed-background-color, var(--active-background-color));
|
||||
border-color: var(--pressed-background-color, var(--active-background-color));
|
||||
}
|
||||
&:is(:active, .active) {
|
||||
&:is(:active, .active:not(.router-link-no-matching-query-flags)) {
|
||||
color:var(--active-color);
|
||||
background-color:var(--active-background-color);
|
||||
border-color: var(--active-background-color);
|
||||
|
@ -607,7 +607,7 @@
|
|||
/* &.active {
|
||||
border-color: var(--link-active-border-color);
|
||||
} */
|
||||
&.router-link-exact-active {
|
||||
&.router-link-exact-active:not(.router-link-no-matching-query-flags) {
|
||||
background-color: var(--exact-active-background-color, var(--active-background-color));
|
||||
}
|
||||
}
|
||||
|
@ -627,7 +627,7 @@
|
|||
background-color: var(--pressed-background-color, var(--active-background-color));
|
||||
border-color: var(--pressed-background-color, var(--active-background-color));
|
||||
}
|
||||
&:is(:active, .active) {
|
||||
&:is(:active, .active:not(.router-link-no-matching-query-flags)) {
|
||||
border-color: var(--active-background-color);
|
||||
&.router-link-exact-active {
|
||||
border-color: var(--exact-active-border-color);
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { computed } from "vue";
|
||||
import { useRouter, type RouteLocationRaw, type LocationQuery } from "vue-router";
|
||||
|
||||
type Assignment<T> = { on: (value : T | null) => string | null, isOn: (value: LocationQuery[string]) => boolean }
|
||||
|
||||
export const exactlyNull:Assignment<unknown> = ({ on: (_) => null, isOn: (value) => value === null })
|
||||
export const notUndefined:Assignment<unknown> = ({ on: (_) => null, isOn: (value) => value !== undefined })
|
||||
|
||||
/**
|
||||
* Bind a modal to a single query parameter in the URL (and vice versa)
|
||||
*
|
||||
|
@ -13,8 +18,7 @@ import { useRouter, type RouteLocationRaw, type LocationQuery } from "vue-router
|
|||
*/
|
||||
export const useModal = <T> (
|
||||
flag: string,
|
||||
assignment: { on: (value : T | null) => string | null, isOn: (value: LocationQuery[string]) => boolean } =
|
||||
{ on: (_) => null, isOn: (value) => value === null }
|
||||
assignment: Assignment<T> = exactlyNull
|
||||
) => {
|
||||
const router = useRouter();
|
||||
const query = computed(() =>
|
||||
|
@ -113,3 +117,9 @@ export const useModal = <T> (
|
|||
|
||||
return { value, isOpen, to, asAttribute, toggle };
|
||||
}
|
||||
|
||||
/* All possible useModals that produce a given `RouterLink` destination */
|
||||
export const fromProps = <T>({to} : { to?: RouteLocationRaw }, assignment: Assignment<T> = exactlyNull): ReturnType<typeof useModal>[] =>
|
||||
to && typeof to !== 'string' && 'query' in to && to.query ?
|
||||
Object.keys(to.query).map( k => useModal(k, assignment) )
|
||||
: []
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
import { useModal, fromProps, notUndefined } from '~/ui/composables/useModal.ts'
|
||||
|
||||
import Modal from '~/components/ui/Modal.vue'
|
||||
import Link from '~/components/ui/Link.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import Layout from '~/components/ui/Layout.vue'
|
||||
import Card from '~/components/ui/Card.vue'
|
||||
import Alert from '~/components/ui/Alert.vue'
|
||||
|
||||
const { to, isOpen } = useModal('flag')
|
||||
</script>
|
||||
|
||||
```ts
|
||||
|
@ -30,6 +35,26 @@ This component will render as [an `<a>` element [MDN]](https://developer.mozil
|
|||
|
||||
Instead of a route, you can set the prop `to` to any web address starting with `http`.
|
||||
|
||||
## `Active` states
|
||||
|
||||
- If any ancestor path matches, the `.router-link-active` class is added
|
||||
- If the whole path matches, the `.router-link-exact-active` class is added
|
||||
|
||||
See the [Vue docs](https://router.vuejs.org/guide/essentials/active-links) for a primer on Path matching.
|
||||
|
||||
In addition to the standard Vue `RouterLink` path matching function, we use this algorithm:
|
||||
|
||||
- If the destination of the link contains any query parameter _and_ none of these is set (i.e. they are all `undefined`), then the class `.router-link-no-matching-query-flags` is added.
|
||||
|
||||
This is particularly useful for modals.
|
||||
|
||||
<Link ghost :to="useModal('flag').to">
|
||||
Open modal
|
||||
</Link>
|
||||
|
||||
<Modal v-model="isOpen" title="Modal">
|
||||
</Modal>
|
||||
|
||||
## Colors and Variants
|
||||
|
||||
See [Using color](/using-color)
|
||||
|
|
Loading…
Reference in New Issue