Skip to content

Commit 0abd66e

Browse files
committed
fix(ComboBox): focus events support
1 parent 7e0f574 commit 0abd66e

File tree

4 files changed

+88
-41
lines changed

4 files changed

+88
-41
lines changed

src/components/fields/ComboBox/ComboBox.stories.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -186,14 +186,16 @@ const meta = {
186186
description: 'Callback fired when the popover opens or closes',
187187
control: { type: null },
188188
},
189-
onBlur: {
190-
action: (e) => ({ type: 'blur', target: e?.target?.tagName }),
191-
description: 'Callback fired when the input loses focus',
189+
onFocus: {
190+
action: 'focus',
191+
description:
192+
'Callback fired when focus enters the ComboBox wrapper element',
192193
control: { type: null },
193194
},
194-
onFocus: {
195-
action: (e) => ({ type: 'focus', target: e?.target?.tagName }),
196-
description: 'Callback fired when the input receives focus',
195+
onBlur: {
196+
action: 'blur',
197+
description:
198+
'Callback fired when focus leaves the ComboBox wrapper element',
197199
control: { type: null },
198200
},
199201
},

src/components/fields/ComboBox/ComboBox.test.tsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,4 +838,49 @@ describe('<ComboBox />', () => {
838838
expect(combobox).toHaveValue('');
839839
});
840840
});
841+
842+
it('should handle wrapper-level focus and blur events', async () => {
843+
const onFocus = jest.fn();
844+
const onBlur = jest.fn();
845+
846+
const { getByRole, getByTestId } = renderWithRoot(
847+
<ComboBox label="test" onFocus={onFocus} onBlur={onBlur}>
848+
{items.map((item) => (
849+
<ComboBox.Item key={item.key}>{item.children}</ComboBox.Item>
850+
))}
851+
</ComboBox>,
852+
);
853+
854+
const combobox = getByRole('combobox');
855+
const trigger = getByTestId('ComboBoxTrigger');
856+
857+
// Focus on input should trigger onFocus
858+
await act(async () => {
859+
combobox.focus();
860+
});
861+
862+
await waitFor(() => {
863+
expect(onFocus).toHaveBeenCalledTimes(1);
864+
});
865+
866+
// Moving focus to trigger button within wrapper should NOT trigger onBlur/onFocus
867+
onFocus.mockClear();
868+
onBlur.mockClear();
869+
870+
await act(async () => {
871+
trigger.focus();
872+
});
873+
874+
expect(onBlur).not.toHaveBeenCalled();
875+
expect(onFocus).not.toHaveBeenCalled();
876+
877+
// Blurring from wrapper should trigger onBlur
878+
await act(async () => {
879+
trigger.blur();
880+
});
881+
882+
await waitFor(() => {
883+
expect(onBlur).toHaveBeenCalledTimes(1);
884+
});
885+
});
841886
});

src/components/fields/ComboBox/ComboBox.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import React, {
1515
} from 'react';
1616
import {
1717
useFilter,
18+
useFocusWithin,
1819
useKeyboard,
1920
useOverlay,
2021
useOverlayPosition,
@@ -128,6 +129,10 @@ export interface CubeComboBoxProps<T>
128129
placeholder?: string;
129130
/** Whether the input should have autofocus */
130131
autoFocus?: boolean;
132+
/** Callback fired when the wrapper element receives focus */
133+
onFocus?: (e: React.FocusEvent) => void;
134+
/** Callback fired when the wrapper element loses focus */
135+
onBlur?: (e: React.FocusEvent) => void;
131136

132137
/** Popover trigger behavior: 'focus', 'input', or 'manual'. Defaults to 'input' */
133138
popoverTrigger?: PopoverTriggerAction;
@@ -913,6 +918,8 @@ export const ComboBox = forwardRef(function ComboBox<T extends object>(
913918
overlayOffset = 8,
914919
onSelectionChange: externalOnSelectionChange,
915920
sortSelectedToTop: sortSelectedToTopProp,
921+
onFocus,
922+
onBlur,
916923
...otherProps
917924
} = props;
918925

@@ -1081,6 +1088,13 @@ export const ComboBox = forwardRef(function ComboBox<T extends object>(
10811088

10821089
const { isFocused, focusProps } = useFocus({ isDisabled });
10831090

1091+
// Wrapper-level focus/blur handlers
1092+
const { focusWithinProps } = useFocusWithin({
1093+
isDisabled,
1094+
onFocusWithin: onFocus,
1095+
onBlurWithin: onBlur,
1096+
});
1097+
10841098
let isInvalid = validationState === 'invalid';
10851099

10861100
let validationIcon = isInvalid ? InvalidIcon : ValidIcon;
@@ -1429,6 +1443,7 @@ export const ComboBox = forwardRef(function ComboBox<T extends object>(
14291443
zIndex: isFocused ? 1 : 'initial',
14301444
}}
14311445
data-size={size}
1446+
{...focusWithinProps}
14321447
>
14331448
{prefix ? <div data-element="Prefix">{prefix}</div> : null}
14341449
<ComboBoxInput

src/components/fields/Switch/Switch.docs.mdx

Lines changed: 20 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,22 @@ Supports [Base properties](/docs/tasty-base-properties--docs)
3636

3737
Customizes the root wrapper element of the component.
3838

39+
#### inputStyles
40+
41+
Customizes the switch toggle element specifically.
42+
3943
**Sub-elements:**
4044
- `Thumb` - The movable indicator inside the switch
4145

42-
#### inputStyles
46+
#### fieldStyles
4347

44-
Customizes the switch toggle element specifically.
48+
Customizes the field wrapper element when the switch is used with labels or validation.
4549

4650
### Style Properties
4751

48-
The Switch component supports all standard style properties:
52+
These properties allow direct style application without using the `styles` prop:
4953

50-
`display`, `font`, `preset`, `hide`, `opacity`, `whiteSpace`, `gridArea`, `order`, `gridColumn`, `gridRow`, `placeSelf`, `alignSelf`, `justifySelf`, `zIndex`, `margin`, `inset`, `position`, `width`, `height`, `flexBasis`, `flexGrow`, `flexShrink`, `flex`, `reset`, `padding`, `paddingInline`, `paddingBlock`, `shadow`, `border`, `radius`, `overflow`, `scrollbar`, `outline`, `textAlign`, `color`, `fill`, `fade`, `textTransform`, `fontWeight`, `fontStyle`, `flow`, `placeItems`, `placeContent`, `alignItems`, `alignContent`, `justifyItems`, `justifyContent`, `align`, `justify`, `gap`, `columnGap`, `rowGap`, `gridColumns`, `gridRows`, `gridTemplate`, `gridAreas`
54+
`margin`, `inset`, `position`, `width`, `height`, `flexBasis`, `flexGrow`, `flexShrink`, `flex`, `padding`, `paddingInline`, `paddingBlock`, `shadow`, `border`, `radius`, `overflow`, `scrollbar`, `outline`
5155

5256
### Modifiers
5357

@@ -69,7 +73,8 @@ The `mods` property accepts the following modifiers you can override:
6973
### Sizes
7074

7175
- `small` - Compact size for dense interfaces
72-
- `default` - Standard size
76+
- `medium` - Standard size (default)
77+
- `large` - Larger size for emphasis
7378

7479
## Examples
7580

@@ -81,11 +86,7 @@ The `mods` property accepts the following modifiers you can override:
8186

8287
### With Default State
8388

84-
```jsx
85-
<Switch defaultSelected={true}>
86-
Auto-save enabled
87-
</Switch>
88-
```
89+
<Story of={SwitchStories.WithDefaultSelected} />
8990

9091
### Controlled State
9192

@@ -100,41 +101,25 @@ const [isEnabled, setIsEnabled] = useState(false);
100101
</Switch>
101102
```
102103

103-
### Small Size
104+
### All Sizes
104105

105-
```jsx
106-
<Switch size="small">
107-
Compact toggle
108-
</Switch>
109-
```
106+
<Story of={SwitchStories.Sizes} />
110107

111108
### With Validation
112109

113-
```jsx
114-
<Switch
115-
isRequired
116-
validationState="invalid"
117-
errorMessage="You must agree to continue"
118-
>
119-
I agree to the terms
120-
</Switch>
121-
```
110+
<Story of={SwitchStories.Invalid} />
122111

123112
### Disabled State
124113

125-
```jsx
126-
<Switch isDisabled>
127-
Cannot be changed
128-
</Switch>
129-
```
114+
<Story of={SwitchStories.Disabled} />
130115

131116
### Loading State
132117

133-
```jsx
134-
<Switch isLoading>
135-
Applying changes...
136-
</Switch>
137-
```
118+
<Story of={SwitchStories.Loading} />
119+
120+
### All States Overview
121+
122+
<Story of={SwitchStories.AllStates} />
138123

139124
## Accessibility
140125

0 commit comments

Comments
 (0)