fix: hover states on filters and chip groups (#9511)
* fix: hover states on filters and chip groups * fix: create FilterChip component
This commit is contained in:
+8
-70
@@ -1,8 +1,8 @@
|
||||
import { EllipseMiniSolid, XMarkMini } from "@medusajs/icons"
|
||||
import { EllipseMiniSolid } from "@medusajs/icons"
|
||||
import { DatePicker, Text, clx } from "@medusajs/ui"
|
||||
import * as Popover from "@radix-ui/react-popover"
|
||||
import isEqual from "lodash/isEqual"
|
||||
import { MouseEvent, useMemo, useState } from "react"
|
||||
import { useMemo, useState } from "react"
|
||||
|
||||
import { t } from "i18next"
|
||||
import { useTranslation } from "react-i18next"
|
||||
@@ -10,6 +10,7 @@ import { useDate } from "../../../../hooks/use-date"
|
||||
import { useSelectedParams } from "../hooks"
|
||||
import { useDataTableFilterContext } from "./context"
|
||||
import { IFilter } from "./types"
|
||||
import FilterChip from "./filter-chip"
|
||||
|
||||
type DateFilterProps = IFilter
|
||||
|
||||
@@ -96,6 +97,8 @@ export const DateFilter = ({
|
||||
|
||||
const displayValue = getDisplayValueFromPresets() || getCustomDisplayValue()
|
||||
|
||||
const [previousValue, setPreviousValue] = useState<string | undefined>(displayValue)
|
||||
|
||||
const handleRemove = () => {
|
||||
selectedParams.delete()
|
||||
removeFilter(key)
|
||||
@@ -105,6 +108,7 @@ export const DateFilter = ({
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setOpen(open)
|
||||
setPreviousValue(displayValue)
|
||||
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
@@ -119,7 +123,8 @@ export const DateFilter = ({
|
||||
|
||||
return (
|
||||
<Popover.Root modal open={open} onOpenChange={handleOpenChange}>
|
||||
<DateDisplay
|
||||
<FilterChip
|
||||
hadPreviousValue={!!previousValue}
|
||||
label={label}
|
||||
value={displayValue}
|
||||
onRemove={handleRemove}
|
||||
@@ -236,73 +241,6 @@ export const DateFilter = ({
|
||||
)
|
||||
}
|
||||
|
||||
type DateDisplayProps = {
|
||||
label: string
|
||||
value?: string
|
||||
readonly?: boolean
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
const DateDisplay = ({
|
||||
label,
|
||||
value,
|
||||
readonly,
|
||||
onRemove,
|
||||
}: DateDisplayProps) => {
|
||||
const handleRemove = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
onRemove()
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover.Trigger
|
||||
asChild
|
||||
className={clx(
|
||||
"bg-ui-bg-field transition-fg shadow-borders-base text-ui-fg-subtle flex cursor-pointer select-none items-center rounded-md",
|
||||
{
|
||||
"hover:bg-ui-bg-field-hover": !readonly,
|
||||
"data-[state=open]:bg-ui-bg-field-hover": !readonly,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={clx("flex items-center justify-center px-2 py-1", {
|
||||
"border-r": !!value,
|
||||
})}
|
||||
>
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{label}
|
||||
</Text>
|
||||
</div>
|
||||
{value && (
|
||||
<div className="flex items-center">
|
||||
<div key={value} className="border-r p-1 px-2">
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{value}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!readonly && value && (
|
||||
<div>
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
className={clx(
|
||||
"text-ui-fg-muted transition-fg flex items-center justify-center p-1",
|
||||
"hover:bg-ui-bg-subtle-hover",
|
||||
"active:bg-ui-bg-subtle-pressed active:text-ui-fg-base"
|
||||
)}
|
||||
>
|
||||
<XMarkMini />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import { XMarkMini } from "@medusajs/icons"
|
||||
import { Text, clx } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { MouseEvent } from "react"
|
||||
import * as Popover from "@radix-ui/react-popover"
|
||||
|
||||
export type FilterChipProps = {
|
||||
hadPreviousValue?: boolean
|
||||
label: string
|
||||
value?: string
|
||||
readonly?: boolean
|
||||
hasOperator?: boolean
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
const FilterChip = ({
|
||||
hadPreviousValue,
|
||||
label,
|
||||
value,
|
||||
readonly,
|
||||
hasOperator,
|
||||
onRemove,
|
||||
}: FilterChipProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleRemove = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
onRemove()
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-ui-bg-field transition-fg shadow-borders-base text-ui-fg-subtle flex cursor-default select-none items-stretch overflow-hidden rounded-md"
|
||||
>
|
||||
{!hadPreviousValue && (
|
||||
<Popover.Anchor />
|
||||
)}
|
||||
<div
|
||||
className={clx(
|
||||
"flex items-center justify-center whitespace-nowrap px-2 py-1",
|
||||
{
|
||||
"border-r": !!(value || hadPreviousValue),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{label}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex w-full items-center overflow-hidden">
|
||||
{hasOperator && !!(value || hadPreviousValue) && (
|
||||
<div className="border-r p-1 px-2">
|
||||
<Text
|
||||
size="small"
|
||||
weight="plus"
|
||||
leading="compact"
|
||||
className="text-ui-fg-muted"
|
||||
>
|
||||
{t("general.is")}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
{!!(value || hadPreviousValue) && (
|
||||
<Popover.Trigger asChild className={clx("flex-1 cursor-pointer overflow-hidden border-r p-1 px-2",
|
||||
{
|
||||
"hover:bg-ui-bg-field-hover": !readonly,
|
||||
"data-[state=open]:bg-ui-bg-field-hover": !readonly,
|
||||
}
|
||||
)}>
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
className="truncate text-nowrap"
|
||||
>
|
||||
{value || "\u00A0"}
|
||||
</Text>
|
||||
</Popover.Trigger>
|
||||
)}
|
||||
</div>
|
||||
{!readonly && !!(value || hadPreviousValue) && (
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
className={clx(
|
||||
"text-ui-fg-muted transition-fg flex items-center justify-center p-1",
|
||||
"hover:bg-ui-bg-subtle-hover",
|
||||
"active:bg-ui-bg-subtle-pressed active:text-ui-fg-base"
|
||||
)}
|
||||
>
|
||||
<XMarkMini />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FilterChip
|
||||
+17
-81
@@ -1,11 +1,10 @@
|
||||
import { EllipseMiniSolid, XMarkMini } from "@medusajs/icons"
|
||||
import { Input, Label, Text, clx } from "@medusajs/ui"
|
||||
import { EllipseMiniSolid } from "@medusajs/icons"
|
||||
import { Input, Label, clx } from "@medusajs/ui"
|
||||
import * as Popover from "@radix-ui/react-popover"
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group"
|
||||
import { debounce } from "lodash"
|
||||
import {
|
||||
ChangeEvent,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
@@ -15,6 +14,8 @@ import { useTranslation } from "react-i18next"
|
||||
import { useSelectedParams } from "../hooks"
|
||||
import { useDataTableFilterContext } from "./context"
|
||||
import { IFilter } from "./types"
|
||||
import { TFunction } from "i18next"
|
||||
import FilterChip from "./filter-chip"
|
||||
|
||||
type NumberFilterProps = IFilter
|
||||
|
||||
@@ -40,6 +41,9 @@ export const NumberFilter = ({
|
||||
})
|
||||
|
||||
const currentValue = selectedParams.get()
|
||||
const [previousValue, setPreviousValue] = useState<string[] | undefined>(
|
||||
currentValue
|
||||
)
|
||||
|
||||
const [operator, setOperator] = useState<Comparison | undefined>(
|
||||
getOperator(currentValue)
|
||||
@@ -99,6 +103,7 @@ export const NumberFilter = ({
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setOpen(open)
|
||||
setPreviousValue(currentValue)
|
||||
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
@@ -131,11 +136,16 @@ export const NumberFilter = ({
|
||||
const LT_KEY = `${key}-lt`
|
||||
const EQ_KEY = key
|
||||
|
||||
const displayValue = parseDisplayValue(currentValue, t)
|
||||
const previousDisplayValue = parseDisplayValue(previousValue, t)
|
||||
|
||||
return (
|
||||
<Popover.Root modal open={open} onOpenChange={handleOpenChange}>
|
||||
<NumberDisplay
|
||||
<FilterChip
|
||||
hasOperator
|
||||
hadPreviousValue={!!previousDisplayValue}
|
||||
label={label}
|
||||
value={currentValue}
|
||||
value={displayValue}
|
||||
onRemove={handleRemove}
|
||||
readonly={readonly}
|
||||
/>
|
||||
@@ -242,23 +252,7 @@ export const NumberFilter = ({
|
||||
)
|
||||
}
|
||||
|
||||
const NumberDisplay = ({
|
||||
label,
|
||||
value,
|
||||
readonly,
|
||||
onRemove,
|
||||
}: {
|
||||
label: string
|
||||
value?: string[]
|
||||
readonly?: boolean
|
||||
onRemove: () => void
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const handleRemove = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
onRemove()
|
||||
}
|
||||
|
||||
const parseDisplayValue = (value: string[] | null | undefined, t: TFunction) => {
|
||||
const parsed = JSON.parse(value?.join(",") || "{}")
|
||||
let displayValue = ""
|
||||
|
||||
@@ -283,65 +277,7 @@ const NumberDisplay = ({
|
||||
displayValue = parsed.toString()
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover.Trigger
|
||||
asChild
|
||||
className={clx(
|
||||
"bg-ui-bg-field transition-fg shadow-borders-base text-ui-fg-subtle flex cursor-pointer select-none items-center rounded-md",
|
||||
{
|
||||
"hover:bg-ui-bg-field-hover": !readonly,
|
||||
"data-[state=open]:bg-ui-bg-field-hover": !readonly,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={clx("flex items-center justify-center px-2 py-1", {
|
||||
"border-r": !!value,
|
||||
})}
|
||||
>
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{label}
|
||||
</Text>
|
||||
</div>
|
||||
{!!value && (
|
||||
<div className="border-r p-1 px-2">
|
||||
<Text
|
||||
size="small"
|
||||
weight="plus"
|
||||
leading="compact"
|
||||
className="text-ui-fg-muted"
|
||||
>
|
||||
{t("general.is")}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
{value && (
|
||||
<div className="flex items-center">
|
||||
<div className="border-r p-1 px-2">
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{displayValue}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!readonly && value && (
|
||||
<div>
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
className={clx(
|
||||
"text-ui-fg-muted transition-fg flex items-center justify-center p-1",
|
||||
"hover:bg-ui-bg-subtle-hover",
|
||||
"active:bg-ui-bg-subtle-pressed active:text-ui-fg-base"
|
||||
)}
|
||||
>
|
||||
<XMarkMini />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Trigger>
|
||||
)
|
||||
return displayValue
|
||||
}
|
||||
|
||||
const parseValue = (value: string[] | null | undefined) => {
|
||||
|
||||
+14
-94
@@ -1,13 +1,14 @@
|
||||
import { CheckMini, EllipseMiniSolid, XMarkMini } from "@medusajs/icons"
|
||||
import { Text, clx } from "@medusajs/ui"
|
||||
import { clx } from "@medusajs/ui"
|
||||
import * as Popover from "@radix-ui/react-popover"
|
||||
import { Command } from "cmdk"
|
||||
import { MouseEvent, useState } from "react"
|
||||
import { useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { useSelectedParams } from "../hooks"
|
||||
import { useDataTableFilterContext } from "./context"
|
||||
import { IFilter } from "./types"
|
||||
import FilterChip from "./filter-chip"
|
||||
|
||||
interface SelectFilterProps extends IFilter {
|
||||
options: { label: string; value: unknown }[]
|
||||
@@ -40,6 +41,8 @@ export const SelectFilter = ({
|
||||
.map((v) => options.find((o) => o.value === v)?.label)
|
||||
.filter(Boolean) as string[]
|
||||
|
||||
const [previousValue, setPreviousValue] = useState<string | string[] | undefined>(labelValues)
|
||||
|
||||
const handleRemove = () => {
|
||||
selectedParams.delete()
|
||||
removeFilter(key)
|
||||
@@ -50,6 +53,8 @@ export const SelectFilter = ({
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setOpen(open)
|
||||
|
||||
setPreviousValue(labelValues)
|
||||
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
@@ -79,12 +84,17 @@ export const SelectFilter = ({
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedValues = labelValues ? (Array.isArray(labelValues) ? labelValues : [labelValues]) : null
|
||||
const normalizedPrev = previousValue ? (Array.isArray(previousValue) ? previousValue : [previousValue]) : null
|
||||
|
||||
return (
|
||||
<Popover.Root modal open={open} onOpenChange={handleOpenChange}>
|
||||
<SelectDisplay
|
||||
<FilterChip
|
||||
hasOperator
|
||||
hadPreviousValue={!!normalizedPrev?.length}
|
||||
readonly={readonly}
|
||||
label={label}
|
||||
value={labelValues}
|
||||
value={normalizedValues?.join(", ")}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
{!readonly && (
|
||||
@@ -179,93 +189,3 @@ export const SelectFilter = ({
|
||||
</Popover.Root>
|
||||
)
|
||||
}
|
||||
|
||||
type SelectDisplayProps = {
|
||||
label: string
|
||||
readonly?: boolean
|
||||
value?: string | string[]
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
export const SelectDisplay = ({
|
||||
label,
|
||||
value,
|
||||
onRemove,
|
||||
readonly,
|
||||
}: SelectDisplayProps) => {
|
||||
const { t } = useTranslation()
|
||||
const v = value ? (Array.isArray(value) ? value : [value]) : null
|
||||
const count = v?.length || 0
|
||||
|
||||
const handleRemove = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
onRemove()
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover.Trigger asChild>
|
||||
<div
|
||||
className={clx(
|
||||
"bg-ui-bg-field transition-fg shadow-borders-base text-ui-fg-subtle flex cursor-pointer select-none items-center overflow-hidden rounded-md",
|
||||
{
|
||||
"hover:bg-ui-bg-field-hover": !readonly,
|
||||
"data-[state=open]:bg-ui-bg-field-hover": !readonly,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clx(
|
||||
"flex items-center justify-center whitespace-nowrap px-2 py-1",
|
||||
{
|
||||
"border-r": count > 0,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{label}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex w-full items-center overflow-hidden">
|
||||
{count > 0 && (
|
||||
<div className="border-r p-1 px-2">
|
||||
<Text
|
||||
size="small"
|
||||
weight="plus"
|
||||
leading="compact"
|
||||
className="text-ui-fg-muted"
|
||||
>
|
||||
{t("general.is")}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
{count > 0 && (
|
||||
<div className="flex-1 overflow-hidden border-r p-1 px-2">
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
className="truncate text-nowrap"
|
||||
>
|
||||
{v?.join(", ")}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!readonly && v && v.length > 0 && (
|
||||
<div>
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
className={clx(
|
||||
"text-ui-fg-muted transition-fg flex items-center justify-center p-1",
|
||||
"hover:bg-ui-bg-subtle-hover",
|
||||
"active:bg-ui-bg-subtle-pressed active:text-ui-fg-base"
|
||||
)}
|
||||
>
|
||||
<XMarkMini />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
+8
-85
@@ -1,12 +1,11 @@
|
||||
import { XMarkMini } from "@medusajs/icons"
|
||||
import { Input, Label, Text, clx } from "@medusajs/ui"
|
||||
import { Input, Label, clx } from "@medusajs/ui"
|
||||
import * as Popover from "@radix-ui/react-popover"
|
||||
import { debounce } from "lodash"
|
||||
import { ChangeEvent, useCallback, useEffect, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useSelectedParams } from "../hooks"
|
||||
import { useDataTableFilterContext } from "./context"
|
||||
import { IFilter } from "./types"
|
||||
import FilterChip from "./filter-chip"
|
||||
|
||||
type StringFilterProps = IFilter
|
||||
|
||||
@@ -25,6 +24,8 @@ export const StringFilter = ({
|
||||
|
||||
const query = selectedParams.get()
|
||||
|
||||
const [previousValue, setPreviousValue] = useState<string | undefined>(query?.[0])
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const debouncedOnChange = useCallback(
|
||||
debounce((e: ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -49,6 +50,7 @@ export const StringFilter = ({
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setOpen(open)
|
||||
setPreviousValue(query?.[0])
|
||||
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
@@ -68,7 +70,9 @@ export const StringFilter = ({
|
||||
|
||||
return (
|
||||
<Popover.Root modal open={open} onOpenChange={handleOpenChange}>
|
||||
<StringDisplay
|
||||
<FilterChip
|
||||
hasOperator
|
||||
hadPreviousValue={!!previousValue}
|
||||
label={label}
|
||||
value={query?.[0]}
|
||||
onRemove={handleRemove}
|
||||
@@ -117,84 +121,3 @@ export const StringFilter = ({
|
||||
</Popover.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const StringDisplay = ({
|
||||
label,
|
||||
value,
|
||||
readonly,
|
||||
onRemove,
|
||||
}: {
|
||||
label: string
|
||||
value?: string
|
||||
readonly?: boolean
|
||||
onRemove: () => void
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Popover.Trigger asChild>
|
||||
<div
|
||||
className={clx(
|
||||
"bg-ui-bg-field transition-fg shadow-borders-base text-ui-fg-subtle flex cursor-pointer select-none items-center overflow-hidden rounded-md",
|
||||
{
|
||||
"hover:bg-ui-bg-field-hover": !readonly,
|
||||
"data-[state=open]:bg-ui-bg-field-hover": !readonly,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clx(
|
||||
"flex items-center justify-center whitespace-nowrap px-2 py-1",
|
||||
{
|
||||
"border-r": !!value,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{label}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex w-full items-center overflow-hidden">
|
||||
{!!value && (
|
||||
<div className="border-r p-1 px-2">
|
||||
<Text
|
||||
size="small"
|
||||
weight="plus"
|
||||
leading="compact"
|
||||
className="text-ui-fg-muted"
|
||||
>
|
||||
{t("general.is")}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
{!!value && (
|
||||
<div className="flex-1 overflow-hidden border-r p-1 px-2">
|
||||
<Text
|
||||
size="small"
|
||||
leading="compact"
|
||||
weight="plus"
|
||||
className="truncate text-nowrap"
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!readonly && !!value && (
|
||||
<div>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className={clx(
|
||||
"text-ui-fg-muted transition-fg flex items-center justify-center p-1",
|
||||
"hover:bg-ui-bg-subtle-hover",
|
||||
"active:bg-ui-bg-subtle-pressed active:text-ui-fg-base"
|
||||
)}
|
||||
>
|
||||
<XMarkMini />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user