Skip to content

Commit d33458b

Browse files
authored
TS strictmode ActionGroup and ActionBar (#3416)
1 parent 168311e commit d33458b

File tree

4 files changed

+56
-46
lines changed

4 files changed

+56
-46
lines changed

packages/@react-aria/utils/src/useResizeObserver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ function hasResizeObserver() {
55
}
66

77
type useResizeObserverOptionsType<T> = {
8-
ref: RefObject<T | undefined>,
8+
ref: RefObject<T | undefined> | undefined,
99
onResize: () => void
1010
}
1111

packages/@react-spectrum/actionbar/src/ActionBar.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,16 @@ function ActionBar<T extends object>(props: SpectrumActionBarProps<T>, ref: DOMR
3737
in={isOpen}
3838
mountOnEnter
3939
unmountOnExit>
40-
<ActionBarInner {...props} ref={ref} />
40+
<ActionBarInnerWithRef {...props} ref={ref} />
4141
</OpenTransition>
4242
);
4343
}
4444

45-
interface ActionBarInnerProps extends SpectrumActionBarProps<unknown> {
45+
interface ActionBarInnerProps<T> extends SpectrumActionBarProps<T> {
4646
isOpen?: boolean
4747
}
4848

49-
const ActionBarInner = React.forwardRef((props: ActionBarInnerProps, ref: DOMRef<HTMLDivElement>) => {
49+
function ActionBarInner<T>(props: ActionBarInnerProps<T>, ref: DOMRef<HTMLDivElement>) {
5050
props = useProviderProps(props);
5151

5252
let {
@@ -101,7 +101,7 @@ const ActionBarInner = React.forwardRef((props: ActionBarInnerProps, ref: DOMRef
101101
<ActionGroup
102102
aria-label={stringFormatter.format('actions')}
103103
isQuiet
104-
staticColor={isEmphasized ? 'white' : null}
104+
staticColor={isEmphasized ? 'white' : undefined}
105105
overflowMode="collapse"
106106
buttonLabelBehavior="collapse"
107107
onAction={onAction}
@@ -113,7 +113,7 @@ const ActionBarInner = React.forwardRef((props: ActionBarInnerProps, ref: DOMRef
113113
aria-label={stringFormatter.format('clearSelection')}
114114
onPress={() => onClearSelection()}
115115
isQuiet
116-
staticColor={isEmphasized ? 'white' : null}>
116+
staticColor={isEmphasized ? 'white' : undefined}>
117117
<CrossLarge />
118118
</ActionButton>
119119
<Text UNSAFE_className={classNames(styles, 'react-spectrum-ActionBar-selectedCount')}>
@@ -125,7 +125,9 @@ const ActionBarInner = React.forwardRef((props: ActionBarInnerProps, ref: DOMRef
125125
</div>
126126
</FocusScope>
127127
);
128-
});
128+
}
129+
130+
const ActionBarInnerWithRef = React.forwardRef(ActionBarInner) as <T>(props: SpectrumActionBarProps<T> & {ref?: DOMRef<HTMLDivElement>}) => ReturnType<typeof ActionBarInner>;
129131

130132
/**
131133
* TODO: Add description of component here.

packages/@react-spectrum/actiongroup/src/ActionGroup.tsx

Lines changed: 45 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -84,36 +84,42 @@ function ActionGroup<T extends object>(props: SpectrumActionGroupProps<T>, ref:
8484
}
8585

8686
let computeVisibleItems = (visibleItems: number) => {
87-
let listItems = Array.from(domRef.current.children) as HTMLLIElement[];
88-
let containerSize = orientation === 'horizontal' ? wrapperRef.current.offsetWidth : wrapperRef.current.offsetHeight;
89-
let isShowingMenu = visibleItems < state.collection.size;
90-
let calculatedSize = 0;
91-
let newVisibleItems = 0;
92-
93-
if (isShowingMenu) {
94-
calculatedSize += orientation === 'horizontal'
95-
? outerWidth(listItems.pop(), false, true)
96-
: outerHeight(listItems.pop(), false, true);
97-
}
87+
if (domRef.current && wrapperRef.current) {
88+
let listItems = Array.from(domRef.current.children) as HTMLLIElement[];
89+
let containerSize = orientation === 'horizontal' ? wrapperRef.current.offsetWidth : wrapperRef.current.offsetHeight;
90+
let isShowingMenu = visibleItems < state.collection.size;
91+
let calculatedSize = 0;
92+
let newVisibleItems = 0;
93+
94+
if (isShowingMenu) {
95+
let item = listItems.pop();
96+
if (item) {
97+
calculatedSize += orientation === 'horizontal'
98+
? outerWidth(item, false, true)
99+
: outerHeight(item, false, true);
100+
}
101+
}
98102

99-
for (let [i, item] of listItems.entries()) {
100-
calculatedSize += orientation === 'horizontal'
101-
? outerWidth(item, i === 0, i === listItems.length - 1)
102-
: outerHeight(item, i === 0, i === listItems.length - 1);
103-
if (calculatedSize <= containerSize) {
104-
newVisibleItems++;
105-
} else {
106-
break;
103+
for (let [i, item] of listItems.entries()) {
104+
calculatedSize += orientation === 'horizontal'
105+
? outerWidth(item, i === 0, i === listItems.length - 1)
106+
: outerHeight(item, i === 0, i === listItems.length - 1);
107+
if (calculatedSize <= containerSize) {
108+
newVisibleItems++;
109+
} else {
110+
break;
111+
}
107112
}
108-
}
109113

110-
// If selection is enabled, and not all of the items fit, collapse all of them into a dropdown
111-
// immediately rather than having some visible and some not.
112-
if (selectionMode !== 'none' && newVisibleItems < state.collection.size) {
113-
return 0;
114-
}
114+
// If selection is enabled, and not all of the items fit, collapse all of them into a dropdown
115+
// immediately rather than having some visible and some not.
116+
if (selectionMode !== 'none' && newVisibleItems < state.collection.size) {
117+
return 0;
118+
}
115119

116-
return newVisibleItems;
120+
return newVisibleItems;
121+
}
122+
return visibleItems;
117123
};
118124

119125
setVisibleItems(function *() {
@@ -166,14 +172,14 @@ function ActionGroup<T extends object>(props: SpectrumActionGroupProps<T>, ref:
166172
// in all scenarios because it may not shrink when available space is reduced.
167173
let parentRef = useMemo(() => ({
168174
get current() {
169-
return wrapperRef.current.parentElement;
175+
return wrapperRef.current?.parentElement;
170176
}
171177
}), [wrapperRef]);
172-
useResizeObserver({ref: overflowMode !== 'wrap' ? parentRef : null, onResize: updateOverflow});
178+
useResizeObserver({ref: overflowMode !== 'wrap' ? parentRef : undefined, onResize: updateOverflow});
173179
useLayoutEffect(updateOverflow, [updateOverflow, state.collection]);
174180

175181
let children = [...state.collection];
176-
let menuItem = null;
182+
let menuItem: ReactElement | null = null;
177183
let menuProps = {};
178184

179185
// If there are no visible items, don't apply any props to the action group container
@@ -257,16 +263,16 @@ export {_ActionGroup as ActionGroup};
257263
interface ActionGroupItemProps<T> extends DOMProps, StyleProps {
258264
item: Node<T>,
259265
state: ListState<T>,
260-
isDisabled: boolean,
261-
isEmphasized: boolean,
266+
isDisabled?: boolean,
267+
isEmphasized?: boolean,
262268
staticColor?: 'white' | 'black',
263269
hideButtonText?: boolean,
264270
orientation?: 'horizontal' | 'vertical',
265-
onAction: (key: Key) => void
271+
onAction?: (key: Key) => void
266272
}
267273

268274
function ActionGroupItem<T>({item, state, isDisabled, isEmphasized, staticColor, onAction, hideButtonText, orientation}: ActionGroupItemProps<T>) {
269-
let ref = useRef();
275+
let ref = useRef(null);
270276
let {buttonProps} = useActionGroupItem({key: item.key}, state);
271277
isDisabled = isDisabled || state.disabledKeys.has(item.key);
272278
let isSelected = state.selectionManager.isSelected(item.key);
@@ -282,7 +288,7 @@ function ActionGroupItem<T>({item, state, isDisabled, isEmphasized, staticColor,
282288
// If button text is hidden, we need to show it as a tooltip instead, so
283289
// go find the text element in the DOM after rendering.
284290
let textId = useId();
285-
let [textContent, setTextContent] = useState('');
291+
let [textContent, setTextContent] = useState<string | null | undefined>('');
286292
useLayoutEffect(() => {
287293
if (hideButtonText) {
288294
setTextContent(document.getElementById(textId)?.textContent);
@@ -359,7 +365,7 @@ interface ActionGroupMenuProps<T> extends AriaLabelingProps {
359365
summaryIcon?: ReactNode,
360366
isOnlyItem?: boolean,
361367
orientation?: 'horizontal' | 'vertical',
362-
onAction: (key: Key) => void
368+
onAction?: (key: Key) => void
363369
}
364370

365371
function ActionGroupMenu<T>({state, isDisabled, isEmphasized, staticColor, items, onAction, summaryIcon, hideButtonText, isOnlyItem, orientation, ...otherProps}: ActionGroupMenuProps<T>) {
@@ -376,7 +382,7 @@ function ActionGroupMenu<T>({state, isDisabled, isEmphasized, staticColor, items
376382
let {hoverProps, isHovered} = useHover({isDisabled});
377383

378384
// If no aria-label or aria-labelledby is given, provide a default one.
379-
let ariaLabel = otherProps['aria-label'] || (otherProps['aria-labelledby'] ? null : '…');
385+
let ariaLabel = otherProps['aria-label'] || (otherProps['aria-labelledby'] ? undefined : '…');
380386
let ariaLabelledby = otherProps['aria-labelledby'];
381387
let textId = useId();
382388
let id = useId();
@@ -392,14 +398,14 @@ function ActionGroupMenu<T>({state, isDisabled, isEmphasized, staticColor, items
392398
let isSelected = state.selectionManager.selectionMode !== 'none' && !state.selectionManager.isEmpty;
393399

394400
// If single selection and empty selection is not allowed, swap the contents of the button to the selected item (like a Picker).
395-
if (!summaryIcon && state.selectionManager.selectionMode === 'single' && state.selectionManager.disallowEmptySelection) {
401+
if (!summaryIcon && state.selectionManager.selectionMode === 'single' && state.selectionManager.disallowEmptySelection && state.selectionManager.firstSelectedKey != null) {
396402
let selectedItem = state.collection.getItem(state.selectionManager.firstSelectedKey);
397403
if (selectedItem) {
398404
summaryIcon = selectedItem.rendered;
399405
if (typeof summaryIcon === 'string') {
400406
summaryIcon = <Text>{summaryIcon}</Text>;
401407
}
402-
iconOnly = hideButtonText;
408+
iconOnly = !!hideButtonText;
403409
ariaLabelledby = `${ariaLabelledby ?? id} ${textId}`;
404410
}
405411
}
@@ -453,7 +459,7 @@ function ActionGroupMenu<T>({state, isDisabled, isEmphasized, staticColor, items
453459
}
454460
isDisabled={isDisabled}
455461
staticColor={staticColor}>
456-
{summaryIcon || <More />}
462+
{summaryIcon ? <More /> : null}
457463
</ActionButton>
458464
</PressResponder>
459465
</SlotProvider>

tsconfig.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
{
3434
"name": "typescript-strict-plugin",
3535
"paths": [
36+
"./packages/@react-spectrum/action",
3637
"./packages/@react-spectrum/button",
3738
"./packages/@react-spectrum/checkbox",
3839
"./packages/@react-spectrum/divider",
@@ -44,6 +45,7 @@
4445
"./packages/@react-spectrum/text",
4546
"./packages/@react-spectrum/view",
4647
"./packages/@react-spectrum/well",
48+
"./packages/@react-types/action",
4749
"./packages/@react-types/button",
4850
"./packages/@react-types/checkbox",
4951
"./packages/@react-types/divider",

0 commit comments

Comments
 (0)