@elliemae/ds-filter-bar 3.70.0-next.3 → 3.70.0-next.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elliemae/ds-filter-bar",
|
|
3
|
-
"version": "3.70.0-next.
|
|
3
|
+
"version": "3.70.0-next.30",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "ICE MT - Dimsum - Filter Bar",
|
|
6
6
|
"files": [
|
|
7
|
-
"dist"
|
|
7
|
+
"dist",
|
|
8
|
+
"skills"
|
|
8
9
|
],
|
|
9
10
|
"module": "./dist/esm/index.js",
|
|
10
11
|
"main": "./dist/cjs/index.js",
|
|
@@ -38,19 +39,19 @@
|
|
|
38
39
|
"dependencies": {
|
|
39
40
|
"@xstyled/system": "~3.8.1",
|
|
40
41
|
"uid": "^2.0.2",
|
|
41
|
-
"@elliemae/ds-
|
|
42
|
-
"@elliemae/ds-
|
|
43
|
-
"@elliemae/ds-menu-button": "3.70.0-next.
|
|
44
|
-
"@elliemae/ds-props-helpers": "3.70.0-next.
|
|
45
|
-
"@elliemae/ds-system": "3.70.0-next.
|
|
46
|
-
"@elliemae/ds-icons": "3.70.0-next.
|
|
42
|
+
"@elliemae/ds-grid": "3.70.0-next.30",
|
|
43
|
+
"@elliemae/ds-button-v2": "3.70.0-next.30",
|
|
44
|
+
"@elliemae/ds-menu-button": "3.70.0-next.30",
|
|
45
|
+
"@elliemae/ds-props-helpers": "3.70.0-next.30",
|
|
46
|
+
"@elliemae/ds-system": "3.70.0-next.30",
|
|
47
|
+
"@elliemae/ds-icons": "3.70.0-next.30"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"jest": "^30.0.0",
|
|
50
51
|
"styled-components": "~5.3.9",
|
|
51
|
-
"@elliemae/ds-test-utils": "3.70.0-next.
|
|
52
|
-
"@elliemae/ds-monorepo-devops": "3.70.0-next.
|
|
53
|
-
"@elliemae/ds-typescript-helpers": "3.70.0-next.
|
|
52
|
+
"@elliemae/ds-test-utils": "3.70.0-next.30",
|
|
53
|
+
"@elliemae/ds-monorepo-devops": "3.70.0-next.30",
|
|
54
|
+
"@elliemae/ds-typescript-helpers": "3.70.0-next.30"
|
|
54
55
|
},
|
|
55
56
|
"peerDependencies": {
|
|
56
57
|
"@testing-library/jest-dom": "^6.6.3",
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ds-filter-bar-health-check
|
|
3
|
+
description: >
|
|
4
|
+
Audit the current state of a @elliemae/ds-filter-bar implementation. Use before starting
|
|
5
|
+
a task that depends on the filter bar, to verify a foundation before building on it, or
|
|
6
|
+
when diagnosing unexpected behavior. Produces a complete state report: what is correct and
|
|
7
|
+
sound, what requires fixing, what is suboptimal, and what gaps may affect the current task.
|
|
8
|
+
type: health-check
|
|
9
|
+
library: ds-filter-bar
|
|
10
|
+
library_version: '3.60.0'
|
|
11
|
+
requires:
|
|
12
|
+
- ds-filter-bar-setup
|
|
13
|
+
- ds-filter-bar-slots
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Purpose
|
|
17
|
+
|
|
18
|
+
This skill audits an existing `@elliemae/ds-filter-bar` integration. It is not a setup guide. It tells you the current state of a filter bar implementation that already exists.
|
|
19
|
+
|
|
20
|
+
**Use it:**
|
|
21
|
+
|
|
22
|
+
- Before starting a task that involves a filter bar — to understand what you are about to touch
|
|
23
|
+
- Before making filter bar interactions load-bearing for a new feature — to verify the foundation
|
|
24
|
+
- When focus, keyboard navigation, or screen reader feedback is broken — to surface what the implementation gets wrong
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Load the embedded knowledge
|
|
29
|
+
|
|
30
|
+
Load the following skills **as reference knowledge only**. You are reading their embedded understanding of correct patterns. You are not running them as tasks.
|
|
31
|
+
|
|
32
|
+
- `ds-filter-bar-setup` — assembly, focus management, `runAfterRemoval`, `ScreenReaderOnly`, `fallbackRef`, `filterBarRef`, state management
|
|
33
|
+
- `ds-filter-bar-slots` — slot injection surface, valid uses, event listener red flag
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Procedure
|
|
38
|
+
|
|
39
|
+
### Step 1 — Locate and read the implementation
|
|
40
|
+
|
|
41
|
+
Find every file where `@elliemae/ds-filter-bar` components are rendered. For each instance:
|
|
42
|
+
|
|
43
|
+
- Record which components are used: `DSFilterBar`, `DSFilterBarContent`, `DSFilterBarActions`, `DSFilterBarMenuButton`
|
|
44
|
+
- Record whether `useFilterBarPrevNextFocus` is imported and used
|
|
45
|
+
- Read where filter state lives (active filters, pills) — state manager, store, atoms, or `useState`
|
|
46
|
+
- Read every `onRemove` handler on pill components — is each one wrapped in `runAfterRemoval`?
|
|
47
|
+
- Read the `onClearFilters` handler on `DSFilterBarMenuButton` — this one should NOT use `runAfterRemoval`
|
|
48
|
+
- Check whether `ScreenReaderOnly aria-live="polite"` is present inside `DSFilterBar`
|
|
49
|
+
- Check whether `filterBarRef` is passed to `DSFilterBar`
|
|
50
|
+
- Check whether `fallbackRef` (or equivalent) is shared between `useFilterBarPrevNextFocus` and `DSFilterBarMenuButton.innerRef`
|
|
51
|
+
- Check whether `RemovableFilterPill` (or equivalent pill component) is defined at module level
|
|
52
|
+
- Check whether the filter bar is inside a DataTable context — if so, `withFilterBar=true` on DataTable may have been the correct choice instead of standalone
|
|
53
|
+
|
|
54
|
+
### Step 2 — Apply diagnostic checks
|
|
55
|
+
|
|
56
|
+
**CRITICAL** — prevents correct function or WCAG failure; fix before proceeding
|
|
57
|
+
**HIGH** — meaningful risk; should fix
|
|
58
|
+
**MEDIUM** — suboptimal; worth addressing
|
|
59
|
+
**SOUND** — correct; rely on it
|
|
60
|
+
|
|
61
|
+
#### Focus management checks
|
|
62
|
+
|
|
63
|
+
- [ ] `useFilterBarPrevNextFocus` is used — without it, focus management is manual and likely incorrect
|
|
64
|
+
- [ ] Every individual pill `onRemove` handler is wrapped in `runAfterRemoval` — omission is a silent WCAG 2.4.3 (Focus Order) failure
|
|
65
|
+
- [ ] `DSFilterBarMenuButton.onClearFilters` is NOT wrapped in `runAfterRemoval` — the menu button persists after clearing; this handler is the correct exception
|
|
66
|
+
- [ ] `filterBarRef` is passed to `DSFilterBar` — without it, the hook cannot scan focusable elements and immediately jumps to `fallbackRef` on every removal
|
|
67
|
+
- [ ] `fallbackRef` (hook) and `innerRef` (DSFilterBarMenuButton) are the same ref object — separate refs leave `fallbackRef.current` null at focus-return time
|
|
68
|
+
|
|
69
|
+
#### Screen reader checks
|
|
70
|
+
|
|
71
|
+
- [ ] `ScreenReaderOnly aria-live="polite"` is rendered inside `DSFilterBar`
|
|
72
|
+
- [ ] The announcement string is updated on every filter state change (remove single, clear all, restore) — not only on one type of change
|
|
73
|
+
- [ ] `ScreenReaderOnly` is imported from `@elliemae/ds-accessibility` — not from `@elliemae/ds-filter-bar`
|
|
74
|
+
|
|
75
|
+
#### Assembly checks
|
|
76
|
+
|
|
77
|
+
- [ ] `RemovableFilterPill` (or equivalent pill wrapper) is defined at module level — not inside the parent component body
|
|
78
|
+
- [ ] Filter state (active filters list) lives in the project's state manager — not `useState`
|
|
79
|
+
- [ ] The one acceptable `useState` is the `announcement` string — ephemeral UI feedback only
|
|
80
|
+
- [ ] No `useEffect` watches filter state to dispatch API calls — consequences belong inside callbacks
|
|
81
|
+
- [ ] If the filter bar is inside a DataTable view: was `withFilterBar=true` on DataTable evaluated first? Standalone `ds-filter-bar` is for non-DataTable contexts
|
|
82
|
+
|
|
83
|
+
#### Slot checks
|
|
84
|
+
|
|
85
|
+
- [ ] No event listeners (`onClick`, `onKeyDown`) injected through slot props — the filter bar manages its own interaction model
|
|
86
|
+
|
|
87
|
+
#### Memoization checks
|
|
88
|
+
|
|
89
|
+
- [ ] `onClearFilters` is wrapped in `useCallback`
|
|
90
|
+
- [ ] `options` for `DSFilterBarMenuButton` is stable (`useMemo` if constructed inline)
|
|
91
|
+
- [ ] Per-pill `onRemove` handlers: each callback is stable (either `useCallback` per item or using a handler factory)
|
|
92
|
+
|
|
93
|
+
### Step 3 — Produce the state report
|
|
94
|
+
|
|
95
|
+
**Foundation — what is correct and sound**
|
|
96
|
+
What the implementation gets right. Be specific — this is what you can rely on.
|
|
97
|
+
|
|
98
|
+
**Critical issues — fix before proceeding**
|
|
99
|
+
Each item with file, location, and pointer to the `ds-filter-bar-setup` skill section covering the fix. A11y failures (runAfterRemoval, ScreenReaderOnly) are always critical.
|
|
100
|
+
|
|
101
|
+
**Warnings — should address, not blocking**
|
|
102
|
+
Each item with context on the risk.
|
|
103
|
+
|
|
104
|
+
**Awareness — no action needed, good to know**
|
|
105
|
+
Suboptimal patterns that are not breaking.
|
|
106
|
+
|
|
107
|
+
**Gaps relevant to this task**
|
|
108
|
+
What the current task will require that is not yet present.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## What this skill does NOT do
|
|
113
|
+
|
|
114
|
+
- It does not generate or modify code
|
|
115
|
+
- It does not run the setup skill as a task
|
|
116
|
+
- It does not replace human judgment on trade-offs
|
|
117
|
+
|
|
118
|
+
## When this isn't enough
|
|
119
|
+
|
|
120
|
+
**ICE internal:** Microsoft Teams — Dimsum channel (informal) / Jira Dimsum board (formal)
|
|
121
|
+
**Partners:** Your organization's Dimsum point of contact
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ds-filter-bar-setup
|
|
3
|
+
description: >
|
|
4
|
+
Filter bar assembly for @elliemae/ds-filter-bar. DSFilterBar (filterBarRef required),
|
|
5
|
+
DSFilterBarContent (pills area), DSFilterBarActions, DSFilterBarMenuButton (onClearFilters,
|
|
6
|
+
options, innerRef). useFilterBarPrevNextFocus hook with filterBarRef + fallbackRef →
|
|
7
|
+
runAfterRemoval wraps every pill onRemove handler. ScreenReaderOnly aria-live="polite"
|
|
8
|
+
required for screen reader announcements. fallbackRef must be passed as both hook
|
|
9
|
+
fallbackRef and DSFilterBarMenuButton innerRef — same ref object.
|
|
10
|
+
type: core
|
|
11
|
+
library: ds-filter-bar
|
|
12
|
+
library_version: '3.60.0'
|
|
13
|
+
sources:
|
|
14
|
+
- '@elliemae/ds-filter-bar:dist/types/react-desc-prop-types.d.ts'
|
|
15
|
+
- '@elliemae/ds-filter-bar:dist/types/hooks/react-desc-prop-types.d.ts'
|
|
16
|
+
- '@elliemae/ds-filter-bar:dist/types/parts/DSFilterBarMenuButton/react-desc-prop-types.d.ts'
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
**Before using standalone `ds-filter-bar`:** if the consuming application already uses `@elliemae/ds-data-table`, check whether `withFilterBar=true` on the DataTable covers the requirement. The DataTable's built-in filter bar handles integration with its own filter state. Standalone `ds-filter-bar` is for filter UIs outside a DataTable context, or in applications that don't use DataTable.
|
|
22
|
+
|
|
23
|
+
Define `RemovableFilterPill` at module level — never inside another component. A component defined inside a parent gets a new type identity on every render, causing React to unmount and remount the subtree on every cycle, which destroys focus mid-removal.
|
|
24
|
+
|
|
25
|
+
Read filter state from the project's state manager (RTK is most common in ICE codebases; adapt to your project's convention — vanilla Redux, Redux-Saga, Zustand, or other). The one acceptable `useState` is the `announcement` string — it is ephemeral UI feedback with no cross-component visibility requirement.
|
|
26
|
+
|
|
27
|
+
`filterBarRef` and `menuButtonRef` must be the same ref objects passed to both the hook and the components. `filterBarRef` wires `DSFilterBar`'s DOM node to the hook's focusable-element scan. `menuButtonRef` is the fallback focus target when no pills remain — it must be passed as `innerRef` to `DSFilterBarMenuButton` so it is populated when the hook needs it.
|
|
28
|
+
|
|
29
|
+
```jsx
|
|
30
|
+
import {
|
|
31
|
+
DSFilterBar,
|
|
32
|
+
DSFilterBarActions,
|
|
33
|
+
DSFilterBarContent,
|
|
34
|
+
DSFilterBarMenuButton,
|
|
35
|
+
useFilterBarPrevNextFocus,
|
|
36
|
+
} from '@elliemae/ds-filter-bar';
|
|
37
|
+
import { ScreenReaderOnly } from '@elliemae/ds-accessibility';
|
|
38
|
+
import { DSPillV2 } from '@elliemae/ds-pills-v2';
|
|
39
|
+
import { useCallback, useRef, useState } from 'react';
|
|
40
|
+
|
|
41
|
+
const RemovableFilterPill = ({ label, onRemove }) => <DSPillV2 type="removable" label={label} onRemove={onRemove} />;
|
|
42
|
+
|
|
43
|
+
const FilterBarComponent = () => {
|
|
44
|
+
const filters = useSelector(selectActiveFilters);
|
|
45
|
+
const dispatch = useDispatch();
|
|
46
|
+
const [announcement, setAnnouncement] = useState('');
|
|
47
|
+
|
|
48
|
+
const filterBarRef = useRef(null);
|
|
49
|
+
const menuButtonRef = useRef(null);
|
|
50
|
+
|
|
51
|
+
const { runAfterRemoval } = useFilterBarPrevNextFocus({
|
|
52
|
+
filterBarRef,
|
|
53
|
+
fallbackRef: menuButtonRef,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const handleSingleFilterRemove = useCallback(
|
|
57
|
+
(filter) => {
|
|
58
|
+
const newFilters = filters.filter((f) => f !== filter);
|
|
59
|
+
dispatch(setFilters(newFilters));
|
|
60
|
+
dispatch(fetchFilteredData(newFilters));
|
|
61
|
+
setAnnouncement(`Filter removed: ${filter}`);
|
|
62
|
+
},
|
|
63
|
+
[dispatch, filters],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const handleClearAll = useCallback(() => {
|
|
67
|
+
dispatch(setFilters([]));
|
|
68
|
+
dispatch(fetchFilteredData([]));
|
|
69
|
+
setAnnouncement('All filters cleared');
|
|
70
|
+
}, [dispatch]);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<DSFilterBar filterBarRef={filterBarRef}>
|
|
74
|
+
<ScreenReaderOnly aria-live="polite">{announcement}</ScreenReaderOnly>
|
|
75
|
+
<DSFilterBarContent>
|
|
76
|
+
{filters.map((filter) => (
|
|
77
|
+
<RemovableFilterPill
|
|
78
|
+
key={filter}
|
|
79
|
+
label={filter}
|
|
80
|
+
onRemove={() => runAfterRemoval(() => handleSingleFilterRemove(filter))}
|
|
81
|
+
/>
|
|
82
|
+
))}
|
|
83
|
+
</DSFilterBarContent>
|
|
84
|
+
<DSFilterBarActions>
|
|
85
|
+
<DSFilterBarMenuButton innerRef={menuButtonRef} onClearFilters={handleClearAll} />
|
|
86
|
+
</DSFilterBarActions>
|
|
87
|
+
</DSFilterBar>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Core Patterns
|
|
93
|
+
|
|
94
|
+
### useFilterBarPrevNextFocus — what it does and why it's required
|
|
95
|
+
|
|
96
|
+
`useFilterBarPrevNextFocus` returns `runAfterRemoval(fn)`. Call it instead of the removal handler directly:
|
|
97
|
+
|
|
98
|
+
1. Before the removal: scans all focusable elements inside the filter bar
|
|
99
|
+
2. Executes the removal function
|
|
100
|
+
3. After the DOM updates (via `requestAnimationFrame`): moves focus to the previous pill, the same index, or the last remaining pill
|
|
101
|
+
4. If no pills remain: focuses `fallbackRef` (the menu button)
|
|
102
|
+
|
|
103
|
+
Without this, keyboard users activating a pill's remove button lose focus when the pill disappears. This is a silent WCAG 2.4.3 (Focus Order) failure — no console error, no axe-core violation on the visible state, but broken keyboard navigation in practice.
|
|
104
|
+
|
|
105
|
+
`runAfterRemoval` is NOT needed for `DSFilterBarMenuButton.onClearFilters`. That action is triggered from inside the menu button, which persists after clearing. Focus stays on the button naturally.
|
|
106
|
+
|
|
107
|
+
**Testing note:** because `runAfterRemoval` uses `requestAnimationFrame` internally, Jest tests that assert focus state after removal may be flaky. If focus assertion tests are needed, use Playwright CT which runs in a real browser.
|
|
108
|
+
|
|
109
|
+
### DSFilterBarMenuButton — options configuration
|
|
110
|
+
|
|
111
|
+
`onClearFilters` is required — it wires the built-in "Clear filters" menu item. `options` adds custom items below it following the DSMenuButton item shape:
|
|
112
|
+
|
|
113
|
+
```jsx
|
|
114
|
+
<DSFilterBarMenuButton
|
|
115
|
+
innerRef={menuButtonRef}
|
|
116
|
+
onClearFilters={handleClearAll}
|
|
117
|
+
options={[
|
|
118
|
+
{
|
|
119
|
+
dsId: 'restore-defaults',
|
|
120
|
+
label: 'Restore default filters',
|
|
121
|
+
type: 'activable-item',
|
|
122
|
+
onClick: () => {
|
|
123
|
+
dispatch(restoreDefaultFilters());
|
|
124
|
+
setAnnouncement('Default filters restored');
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
]}
|
|
128
|
+
/>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Common Mistakes
|
|
132
|
+
|
|
133
|
+
### CRITICAL Not wrapping pill removal handlers with runAfterRemoval
|
|
134
|
+
|
|
135
|
+
Wrong:
|
|
136
|
+
|
|
137
|
+
```jsx
|
|
138
|
+
<RemovableFilterPill label={filter} onRemove={() => handleSingleFilterRemove(filter)} />
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Correct:
|
|
142
|
+
|
|
143
|
+
```jsx
|
|
144
|
+
<RemovableFilterPill label={filter} onRemove={() => runAfterRemoval(() => handleSingleFilterRemove(filter))} />
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
When a pill is removed via keyboard, focus is on the pill's remove button. The pill disappears after removal. Without `runAfterRemoval`, focus is stranded — a silent WCAG 2.4.3 (Focus Order) failure. The hook scans focusable elements before removal and moves focus correctly after the DOM updates.
|
|
148
|
+
|
|
149
|
+
Source: https://dimsum.mortgagetech.ice.com/iframe.html?id=components-filterbar-features--basic
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
### CRITICAL Passing fallbackRef to hook but not as innerRef to DSFilterBarMenuButton
|
|
154
|
+
|
|
155
|
+
Wrong:
|
|
156
|
+
|
|
157
|
+
```jsx
|
|
158
|
+
const menuButtonRef = useRef(null);
|
|
159
|
+
const { runAfterRemoval } = useFilterBarPrevNextFocus({ filterBarRef, fallbackRef: menuButtonRef });
|
|
160
|
+
<DSFilterBarMenuButton onClearFilters={handleClearAll} />;
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Correct:
|
|
164
|
+
|
|
165
|
+
```jsx
|
|
166
|
+
const menuButtonRef = useRef(null);
|
|
167
|
+
const { runAfterRemoval } = useFilterBarPrevNextFocus({ filterBarRef, fallbackRef: menuButtonRef });
|
|
168
|
+
<DSFilterBarMenuButton innerRef={menuButtonRef} onClearFilters={handleClearAll} />;
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`fallbackRef.current` is null until `innerRef` connects it to the DOM element. When the last pill is removed, the hook calls `fallbackRef.current?.focus?.()` — if the ref is null, focus is lost silently. The keyboard user must Tab from the beginning of the page to re-orient.
|
|
172
|
+
|
|
173
|
+
Source: https://dimsum.mortgagetech.ice.com/iframe.html?id=components-filterbar-features--basic
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### HIGH Omitting ScreenReaderOnly aria-live announcements
|
|
178
|
+
|
|
179
|
+
Wrong:
|
|
180
|
+
|
|
181
|
+
```jsx
|
|
182
|
+
const handleSingleFilterRemove = (filter) => {
|
|
183
|
+
dispatch(removeFilter(filter));
|
|
184
|
+
};
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Correct:
|
|
188
|
+
|
|
189
|
+
```jsx
|
|
190
|
+
const [announcement, setAnnouncement] = useState('');
|
|
191
|
+
|
|
192
|
+
const handleSingleFilterRemove = (filter) => {
|
|
193
|
+
dispatch(removeFilter(filter));
|
|
194
|
+
setAnnouncement(`Filter removed: ${filter}`);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Inside DSFilterBar:
|
|
198
|
+
<ScreenReaderOnly aria-live="polite">{announcement}</ScreenReaderOnly>;
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Screen readers do not announce dynamic DOM changes unless explicitly told to. Without `aria-live="polite"`, screen reader users receive no feedback on filter operations. Every state change (remove single, clear all, restore) must update the announcement string. This is a WCAG 4.1.3 (Status Messages) requirement.
|
|
202
|
+
|
|
203
|
+
`ScreenReaderOnly` is from `@elliemae/ds-accessibility` — add this import separately alongside the `@elliemae/ds-filter-bar` imports.
|
|
204
|
+
|
|
205
|
+
Source: https://dimsum.mortgagetech.ice.com/iframe.html?id=components-filterbar-features--basic
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
### HIGH Omitting filterBarRef from DSFilterBar
|
|
210
|
+
|
|
211
|
+
Wrong:
|
|
212
|
+
|
|
213
|
+
```jsx
|
|
214
|
+
const filterBarRef = useRef(null);
|
|
215
|
+
const { runAfterRemoval } = useFilterBarPrevNextFocus({ filterBarRef, fallbackRef });
|
|
216
|
+
<DSFilterBar>...</DSFilterBar>;
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Correct:
|
|
220
|
+
|
|
221
|
+
```jsx
|
|
222
|
+
<DSFilterBar filterBarRef={filterBarRef}>...</DSFilterBar>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Without `filterBarRef`, the hook's container is null. It skips the focusable-elements scan and immediately focuses `fallbackRef` on every removal — correct only for the last pill, wrong for all others.
|
|
226
|
+
|
|
227
|
+
Source: https://dimsum.mortgagetech.ice.com/iframe.html?id=components-filterbar-features--basic
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
### HIGH Non-primitive filter bar props not memoized
|
|
232
|
+
|
|
233
|
+
| Prop | Component | Memoization |
|
|
234
|
+
| ---------------- | ----------------------- | ------------------------------- |
|
|
235
|
+
| `onClearFilters` | `DSFilterBarMenuButton` | `useCallback` |
|
|
236
|
+
| `options` | `DSFilterBarMenuButton` | `useMemo` if constructed inline |
|
|
237
|
+
| `filterBarRef` | `DSFilterBar` | `useRef` (stable by definition) |
|
|
238
|
+
| `innerRef` | `DSFilterBarMenuButton` | `useRef` (stable by definition) |
|
|
239
|
+
|
|
240
|
+
Source: https://dimsum.mortgagetech.ice.com/iframe.html?id=components-filterbar-features--basic
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### HIGH Defining RemovableFilterPill inside parent component
|
|
245
|
+
|
|
246
|
+
Wrong:
|
|
247
|
+
|
|
248
|
+
```jsx
|
|
249
|
+
const FilterBarComponent = () => {
|
|
250
|
+
const RemovableFilterPill = ({ label, onRemove }) => <DSPillV2 type="removable" label={label} onRemove={onRemove} />;
|
|
251
|
+
// ...
|
|
252
|
+
};
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Correct:
|
|
256
|
+
|
|
257
|
+
```jsx
|
|
258
|
+
const RemovableFilterPill = ({ label, onRemove }) => <DSPillV2 type="removable" label={label} onRemove={onRemove} />;
|
|
259
|
+
|
|
260
|
+
const FilterBarComponent = () => {
|
|
261
|
+
/* ... */
|
|
262
|
+
};
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
A component defined inside another component gets a new type identity on every render. React treats each new identity as a different component, unmounting and remounting the subtree — this destroys internal state, re-triggers effects, and causes focus to be lost during the render cycle that follows a pill removal.
|
|
266
|
+
|
|
267
|
+
Source: https://dimsum.mortgagetech.ice.com/iframe.html?id=components-filterbar-features--basic
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
### HIGH Updating filter state without propagating to the data source
|
|
272
|
+
|
|
273
|
+
The filter bar is UI-only — removing a pill has no effect on displayed data unless the consumer propagates the change to the data source. Before implementing, confirm whether the model is server-side (each filter change dispatches an API call — correct for any non-trivial dataset) or client-side (filters applied to an in-memory array — a temporary workaround while backend endpoints are built, not a design pattern). Client-side filtering is a legitimate interim state when explicitly owned by the team with a documented remediation plan; it becomes a problem when treated as the default.
|
|
274
|
+
|
|
275
|
+
Source: https://dimsum.mortgagetech.ice.com/iframe.html?id=components-filterbar-features--basic
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## When composable parts are not enough
|
|
280
|
+
|
|
281
|
+
If the filter bar assembly cannot satisfy the layout or behavior requirement:
|
|
282
|
+
|
|
283
|
+
1. Confirm the requirement with UI/UX — most variations are achievable via composition, ordering, and xstyled props on the primitives.
|
|
284
|
+
2. Contact the Dimsum team to validate whether a first-class solution should be added.
|
|
285
|
+
|
|
286
|
+
**ICE internal:** Microsoft Teams — Dimsum channel (informal) / Jira Dimsum board (formal)
|
|
287
|
+
**Partners:** Your organization's Dimsum point of contact
|
|
288
|
+
|
|
289
|
+
See also: `@elliemae/ds-data-table` `withFilterBar` integration — if this filter bar is used alongside a DataTable, load the DataTable filtering skill for the integration wiring.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ds-filter-bar-slots
|
|
3
|
+
description: >
|
|
4
|
+
Slot injection surface for @elliemae/ds-filter-bar. Verified against v3.60.0 via probe.
|
|
5
|
+
DSFilterBar (ROOT), DSFilterBarContent (CONTENT), DSFilterBarActions (ACTIONS) have
|
|
6
|
+
functional slot injection. DSFilterBarMenuButton slot props have no effect — use its
|
|
7
|
+
OOB props (innerRef, onClearFilters, options) instead. Valid uses: data-*, aria-*,
|
|
8
|
+
conditional styling. Event listeners through slots are a red flag.
|
|
9
|
+
type: core
|
|
10
|
+
library: ds-filter-bar
|
|
11
|
+
library_version: '3.60.0'
|
|
12
|
+
requires:
|
|
13
|
+
- ds-filter-bar-setup
|
|
14
|
+
sources:
|
|
15
|
+
- '@elliemae/ds-filter-bar:dist/types/react-desc-prop-types.d.ts'
|
|
16
|
+
- '@elliemae/ds-filter-bar:dist/types/parts/DSFilterBarMenuButton/react-desc-prop-types.d.ts'
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Slot surface — DOM hierarchy
|
|
20
|
+
|
|
21
|
+
Slots are injection points into specific rendered DOM nodes. Understanding the hierarchy is required to inject props at the correct level — injecting `aria-*` at the wrong node produces silent accessibility failures.
|
|
22
|
+
|
|
23
|
+
**About this map:** Verified against `@elliemae/ds-filter-bar@3.60.0` via probe tests. Slot names are governed by the Dimsum breaking change protocol — renames are intentional and documented. If something doesn't match your installed version, inspect the rendered HTML in your environment. If you cannot do it autonomously, ask the human in the loop to render the component with synthetic data only — not production data containing real user information.
|
|
24
|
+
|
|
25
|
+
Each entry shows: `slot-key (data-dimsum-slot="value") · tag[role] · notes`.
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
DSFilterBar renders:
|
|
29
|
+
|
|
30
|
+
ROOT (dsFilterbarRoot) · div[role="region"] ← ★ semantic region landmark
|
|
31
|
+
│ inject aria-label here — it labels the landmark for screen readers
|
|
32
|
+
│ inject data-* tracking attributes here
|
|
33
|
+
├── CONTENT (dsFilterbarContent) · div
|
|
34
|
+
│ └── (pill components — DSPillV2 or custom)
|
|
35
|
+
└── ACTIONS (dsFilterbarActions) · div
|
|
36
|
+
└── DSFilterBarMenuButton ← no dsFilterbarMenuButton in DOM
|
|
37
|
+
└── dsMenubuttonRoot · div (the DOM shows this instead of dsFilterbarMenuButton)
|
|
38
|
+
└── dsButtonRoot · button
|
|
39
|
+
└── dsIconRoot · span
|
|
40
|
+
└── dsIconSvg · svg
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Key finding — ROOT has `role="region"`:** `dsFilterbarRoot` renders as a semantic region landmark. This means:
|
|
44
|
+
|
|
45
|
+
- Screen readers announce it as a navigable region
|
|
46
|
+
- `aria-label` passed to `DSFilterBar` (or via `dsFilterbarRoot` slot) labels that region
|
|
47
|
+
- Do not inject a second `role` attribute — it is already set correctly
|
|
48
|
+
|
|
49
|
+
## When to reach for slots
|
|
50
|
+
|
|
51
|
+
Try OOB first. `DSFilterBar` accepts standard HTML global attributes (`aria-*`, `data-*`, `className`, `style`) directly as props. Slots are for injecting into a specific internal DOM node rather than the root.
|
|
52
|
+
|
|
53
|
+
**Event listeners through slots are a red flag.** The filter bar manages its own interaction model. If you need to respond to filter bar interactions, use the OOB callbacks (`onRemove`, `onClearFilters`). If no OOB callback covers your requirement, stop and surface this to the human in the loop.
|
|
54
|
+
|
|
55
|
+
## Setup
|
|
56
|
+
|
|
57
|
+
```jsx
|
|
58
|
+
import {
|
|
59
|
+
DSFilterBar,
|
|
60
|
+
DSFilterBarContent,
|
|
61
|
+
DSFilterBarActions,
|
|
62
|
+
} from '@elliemae/ds-filter-bar';
|
|
63
|
+
|
|
64
|
+
// Inject into the region root
|
|
65
|
+
<DSFilterBar
|
|
66
|
+
filterBarRef={filterBarRef}
|
|
67
|
+
dsFilterbarRoot={{ 'aria-label': 'Active loan filters', 'data-section': 'filters' }}
|
|
68
|
+
>
|
|
69
|
+
...
|
|
70
|
+
</DSFilterBar>
|
|
71
|
+
|
|
72
|
+
// Inject into the content area
|
|
73
|
+
<DSFilterBarContent dsFilterbarContent={{ 'data-pills-zone': 'true' }}>
|
|
74
|
+
...
|
|
75
|
+
</DSFilterBarContent>
|
|
76
|
+
|
|
77
|
+
// Inject into the actions area
|
|
78
|
+
<DSFilterBarActions dsFilterbarActions={{ 'data-actions-zone': 'true' }}>
|
|
79
|
+
...
|
|
80
|
+
</DSFilterBarActions>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### DSFilterBarMenuButton — no dsFilterbarMenuButton in DOM
|
|
84
|
+
|
|
85
|
+
`dsFilterbarMenuButton` does not appear in the DOM — the DOM shows `dsMenubuttonRoot` in its place. Passing `dsFilterbarMenuButton` slot props has no effect. Use `innerRef`, `onClearFilters`, and `options` props to interact with this component instead.
|
|
86
|
+
|
|
87
|
+
## Common Mistakes
|
|
88
|
+
|
|
89
|
+
### HIGH Injecting event listeners through slot props
|
|
90
|
+
|
|
91
|
+
The filter bar manages its own interaction model. Injecting `onClick`, `onKeyDown`, or similar through slot props bypasses it and can produce unpredictable behavior. Use OOB callbacks (`onRemove`, `onClearFilters`) instead. If no OOB callback exists for your requirement, surface this to the human in the loop before proceeding.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### MEDIUM Slot props not memoized
|
|
96
|
+
|
|
97
|
+
```jsx
|
|
98
|
+
// Wrong — new object reference on every render
|
|
99
|
+
<DSFilterBar dsFilterbarRoot={{ 'data-id': computedId }} />;
|
|
100
|
+
|
|
101
|
+
// Correct
|
|
102
|
+
const rootSlotProps = useMemo(() => ({ 'data-id': computedId }), [computedId]);
|
|
103
|
+
<DSFilterBar dsFilterbarRoot={rootSlotProps} />;
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## When this isn't enough
|
|
107
|
+
|
|
108
|
+
**ICE internal:** Microsoft Teams — Dimsum channel (informal) / Jira Dimsum board (formal)
|
|
109
|
+
**Partners:** Your organization's Dimsum point of contact
|