Skip to content

Commit

Permalink
FilterBar: manage focus on add/remove filter
Browse files Browse the repository at this point in the history
  • Loading branch information
HeartSquared authored and dougmacknz committed Oct 21, 2024
1 parent 03bc020 commit d499fef
Show file tree
Hide file tree
Showing 10 changed files with 72 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .changeset/many-plums-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kaizen/components": patch
---

FilterBar: manage focus on add/remove filters
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export type FilterBarContextValue<
hideFilter: <Id extends keyof ValuesMap>(id: Id) => void
getInactiveFilters: () => Array<FilterAttributes<ValuesMap>>
clearAllFilters: () => void
setFocus: <Id extends keyof ValuesMap>(id: Id | undefined) => void
focusId: keyof ValuesMap | undefined
}

const FilterBarContext = React.createContext<FilterBarContextValue<any> | null>(
Expand Down Expand Up @@ -119,10 +121,13 @@ export const FilterBarProvider = <ValuesMap extends FiltersValues>({
values: { ...values, [id]: getValidValue(newValue) },
})
},
showFilter: <Id extends keyof ValuesMap>(id: Id): void =>
dispatch({ type: "activate_filter", id }),
showFilter: <Id extends keyof ValuesMap>(id: Id): void => {
dispatch({ type: "activate_filter", id })
dispatch({ type: "set_focus", id })
},
hideFilter: <Id extends keyof ValuesMap>(id: Id): void => {
dispatch({ type: "deactivate_filter", id })
dispatch({ type: "set_focus", id: "add_filter" })
},
getInactiveFilters: () => getInactiveFilters<ValuesMap>(state),
clearAllFilters: () => {
Expand All @@ -132,6 +137,10 @@ export const FilterBarProvider = <ValuesMap extends FiltersValues>({
})
dispatch({ type: "update_values", values: {} })
},
setFocus: <Id extends keyof ValuesMap>(id: Id | undefined) => {
dispatch({ type: "set_focus", id })
},
focusId: state.focusId,
} satisfies FilterBarContextValue<any, ValuesMap>

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,19 @@ type Actions<ValuesMap extends FiltersValues> =
| { type: "activate_filter"; id: keyof ValuesMap }
| { type: "deactivate_filter"; id: keyof ValuesMap }
| { type: "update_filter_labels"; data: Filters<ValuesMap> }
| { type: "set_focus"; id: keyof ValuesMap }

export const filterBarStateReducer = <ValuesMap extends FiltersValues>(
state: FilterBarState<ValuesMap>,
action: Actions<ValuesMap>
): FilterBarState<ValuesMap> => {
switch (action.type) {
case "set_focus":
return {
...state,
focusId: action.id,
}

case "update_values":
return { ...updateValues(state, action.values) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const setupFilterBarState = <ValuesMap extends FiltersValues>(
values,
dependentFilterIds: new Set(),
hasUpdatedValues: false,
focusId: undefined,
} as FilterBarState<ValuesMap>
)

Expand Down
1 change: 1 addition & 0 deletions packages/components/src/Filter/FilterBar/context/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export type FilterBarState<ValuesMap extends FiltersValues> = {
activeFilterIds: Set<keyof ValuesMap>
values: Partial<ValuesMap>
dependentFilterIds: Set<keyof ValuesMap>
focusId?: keyof ValuesMap | undefined
}

export type ActiveFiltersArray<ValuesMap extends FiltersValues> = Array<
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React from "react"
import React, { useEffect, useRef } from "react"
import { useIntl } from "@cultureamp/i18n-react-intl"
import { Menu, MenuList, MenuItem, Button } from "~components/__actions__/v2"
import { Icon } from "~components/__future__/Icon"
import { useFilterBarContext } from "../../context/FilterBarContext"

export const AddFiltersMenu = (): JSX.Element => {
const { formatMessage } = useIntl()
const buttonRef = useRef<HTMLButtonElement>(null)

const menuButtonLabel = formatMessage({
id: "filterBar.addFiltersMenu.buttonLabel",
Expand All @@ -14,13 +15,22 @@ export const AddFiltersMenu = (): JSX.Element => {
"Menu button label to show additional available filter options",
})

const { getInactiveFilters, showFilter } = useFilterBarContext()
const { getInactiveFilters, showFilter, focusId, setFocus } =
useFilterBarContext()
const inactiveFilters = getInactiveFilters()

useEffect(() => {
if (focusId === "add_filter") {
buttonRef.current?.focus()
setFocus(undefined)
}
}, [focusId])

return (
<Menu
button={
<Button
ref={buttonRef}
label={menuButtonLabel}
secondary
disabled={inactiveFilters.length === 0}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { forwardRef } from "react"
import React, { forwardRef, useEffect } from "react"
import { FilterTriggerRef } from "~components/Filter/Filter"
import { useFilterBarContext } from "~components/Filter/FilterBar/context/FilterBarContext"
import {
Expand All @@ -16,7 +16,15 @@ export const FilterBarButton = forwardRef<
FilterTriggerRef,
FilterBarButtonProps
>(({ filterId, isRemovable = false, ...props }, ref): JSX.Element => {
const { hideFilter } = useFilterBarContext()
const { hideFilter, focusId, setFocus } = useFilterBarContext()

useEffect(() => {
if (focusId === filterId) {
// @ts-ignore
ref?.current?.triggerRef?.current.focus()
setFocus(undefined)
}
}, [focusId])

return isRemovable ? (
<FilterButtonRemovable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"
import React, { useEffect, useRef, useState } from "react"
import { Selection, Key } from "@react-types/shared"
import {
FilterMultiSelect,
Expand Down Expand Up @@ -43,9 +43,16 @@ export const FilterBarMultiSelect = ({
onSelectionChange,
...props
}: FilterBarMultiSelectProps): JSX.Element | null => {
const { getFilterState, setFilterOpenState, updateValue, hideFilter } =
useFilterBarContext<ConsumableSelection>()
const {
getFilterState,
setFilterOpenState,
updateValue,
hideFilter,
focusId,
setFocus,
} = useFilterBarContext<ConsumableSelection>()
const [items, setItems] = useState<ItemType[]>(propsItems)
const buttonRef = useRef<HTMLButtonElement>(null)

if (!id) throw Error("Missing `id` prop in FilterBarMultiSelect")

Expand All @@ -70,6 +77,13 @@ export const FilterBarMultiSelect = ({
}
}, [items])

useEffect(() => {
if (focusId === id) {
buttonRef.current?.focus()
setFocus(undefined)
}
}, [focusId])

return (
<FilterMultiSelect
label={filterState.name}
Expand Down Expand Up @@ -103,6 +117,7 @@ export const FilterBarMultiSelect = ({
<FilterMultiSelect.TriggerButton {...triggerProps} />
)
}}
triggerRef={buttonRef}
{...props}
>
{children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type SelectionProps = {
export type FilterMultiSelectProps = {
trigger: (value?: MenuTriggerProviderContextType) => React.ReactNode
children: (value?: SelectionProviderContextType) => React.ReactNode // the content of the menu
triggerRef?: React.RefObject<HTMLButtonElement>
} & Omit<MenuPopupProps, "children"> &
Omit<MenuTriggerProviderProps, "children"> &
SelectionProps
Expand All @@ -60,8 +61,9 @@ export const FilterMultiSelect = ({
onSelectionChange,
selectionMode = "multiple",
onSearchInputChange,
triggerRef,
}: FilterMultiSelectProps): JSX.Element => {
const menuTriggerProps = { isOpen, defaultOpen, onOpenChange }
const menuTriggerProps = { isOpen, defaultOpen, onOpenChange, triggerRef }
const menuPopupProps = { isLoading, loadingSkeleton }
const disabledKeys: Selection = new Set(
items
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type MenuTriggerProviderProps = {
defaultOpen?: boolean
onOpenChange?: (isOpen: boolean) => void
children: React.ReactNode
triggerRef?: React.RefObject<HTMLButtonElement>
}

export type MenuTriggerProviderContextType = {
Expand All @@ -32,12 +33,14 @@ export function MenuTriggerProvider({
defaultOpen,
onOpenChange,
children,
triggerRef,
}: MenuTriggerProviderProps): JSX.Element {
// Create state based on the incoming props to manage the open/close
const state = useMenuTriggerState({ isOpen, defaultOpen, onOpenChange })

// Get A11y attributes and events for the menu trigger and menu elements
const ref = useRef<HTMLButtonElement>(null)
const fallbackRef = useRef<HTMLButtonElement>(null)
const ref = triggerRef || fallbackRef
const { menuTriggerProps, menuProps } = useMenuTrigger<ItemType>(
{},
state,
Expand Down

0 comments on commit d499fef

Please sign in to comment.