146 lines
5.1 KiB
JavaScript
146 lines
5.1 KiB
JavaScript
'use client';
|
|
import * as React from 'react';
|
|
import { getIntrinsicElementProps, useId, useMergedRefs, slot } from '@fluentui/react-utilities';
|
|
import { useActiveDescendantContext } from '@fluentui/react-aria';
|
|
import { CheckmarkFilled, Checkmark12Filled } from '@fluentui/react-icons';
|
|
import { useListboxContext_unstable } from '../../contexts/ListboxContext';
|
|
function getTextString(text, children) {
|
|
if (text !== undefined) {
|
|
return text;
|
|
}
|
|
let textString = '';
|
|
let hasNonStringChild = false;
|
|
React.Children.forEach(children, (child)=>{
|
|
if (typeof child === 'string') {
|
|
textString += child;
|
|
} else {
|
|
hasNonStringChild = true;
|
|
}
|
|
});
|
|
// warn if an Option has non-string children and no text prop
|
|
if (hasNonStringChild) {
|
|
// eslint-disable-next-line no-console
|
|
console.warn('Provide a `text` prop to Option components when they contain non-string children.');
|
|
}
|
|
return textString;
|
|
}
|
|
/**
|
|
* Create the state required to render Option.
|
|
*
|
|
* The returned state can be modified with hooks such as useOptionStyles_unstable,
|
|
* before being passed to renderOption_unstable.
|
|
*
|
|
* @param props - props from this instance of Option
|
|
* @param ref - reference to root HTMLElement of Option
|
|
*/ export const useOption_unstable = (props, ref)=>{
|
|
'use no memo';
|
|
const state = useOptionBase_unstable(props, ref);
|
|
// check icon
|
|
let CheckIcon = /*#__PURE__*/ React.createElement(CheckmarkFilled, null);
|
|
if (state.multiselect) {
|
|
CheckIcon = state.selected ? /*#__PURE__*/ React.createElement(Checkmark12Filled, null) : '';
|
|
}
|
|
if (state.checkIcon) {
|
|
var _state_checkIcon;
|
|
var _children;
|
|
(_children = (_state_checkIcon = state.checkIcon).children) !== null && _children !== void 0 ? _children : _state_checkIcon.children = CheckIcon;
|
|
}
|
|
return state;
|
|
};
|
|
/**
|
|
* Create the base state required to render Option.
|
|
*
|
|
* @param props - props from this instance of Option
|
|
* @param ref - reference to root HTMLElement of Option
|
|
*/ export const useOptionBase_unstable = (props, ref)=>{
|
|
const { children, disabled, text, value } = props;
|
|
const optionRef = React.useRef(null);
|
|
const optionText = getTextString(text, children);
|
|
const optionValue = value !== null && value !== void 0 ? value : optionText;
|
|
// use the id if provided, otherwise use a generated id
|
|
const id = useId('fluent-option', props.id);
|
|
// data used for context registration & events
|
|
const optionData = React.useMemo(()=>({
|
|
id,
|
|
disabled,
|
|
text: optionText,
|
|
value: optionValue
|
|
}), [
|
|
id,
|
|
disabled,
|
|
optionText,
|
|
optionValue
|
|
]);
|
|
// context values
|
|
const { controller: activeDescendantController } = useActiveDescendantContext();
|
|
const multiselect = useListboxContext_unstable((ctx)=>ctx.multiselect);
|
|
const registerOption = useListboxContext_unstable((ctx)=>ctx.registerOption);
|
|
const selected = useListboxContext_unstable((ctx)=>{
|
|
const selectedOptions = ctx.selectedOptions;
|
|
return optionValue !== undefined && selectedOptions.find((o)=>o === optionValue) !== undefined;
|
|
});
|
|
const selectOption = useListboxContext_unstable((ctx)=>ctx.selectOption);
|
|
const onOptionClick = useListboxContext_unstable((ctx)=>ctx.onOptionClick);
|
|
const onClick = (event)=>{
|
|
var _props_onClick;
|
|
if (disabled) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
activeDescendantController.focus(id);
|
|
// handle selection change
|
|
selectOption(event, optionData);
|
|
onOptionClick(event);
|
|
(_props_onClick = props.onClick) === null || _props_onClick === void 0 ? void 0 : _props_onClick.call(props, event);
|
|
};
|
|
// register option data with context
|
|
React.useEffect(()=>{
|
|
if (id && optionRef.current) {
|
|
return registerOption(optionData, optionRef.current);
|
|
}
|
|
}, [
|
|
id,
|
|
optionData,
|
|
registerOption
|
|
]);
|
|
const semanticProps = multiselect ? {
|
|
role: 'menuitemcheckbox',
|
|
'aria-checked': selected
|
|
} : {
|
|
role: 'option',
|
|
'aria-selected': selected
|
|
};
|
|
return {
|
|
components: {
|
|
root: 'div',
|
|
checkIcon: 'span'
|
|
},
|
|
root: slot.always(getIntrinsicElementProps('div', {
|
|
// FIXME:
|
|
// `ref` is wrongly assigned to be `HTMLElement` instead of `HTMLDivElement`
|
|
// but since it would be a breaking change to fix it, we are casting ref to it's proper type
|
|
ref: useMergedRefs(ref, optionRef),
|
|
'aria-disabled': disabled ? 'true' : undefined,
|
|
id,
|
|
...semanticProps,
|
|
...props,
|
|
onClick
|
|
}), {
|
|
elementType: 'div'
|
|
}),
|
|
checkIcon: slot.optional(props.checkIcon, {
|
|
renderByDefault: true,
|
|
defaultProps: {
|
|
'aria-hidden': 'true'
|
|
},
|
|
elementType: 'span'
|
|
}),
|
|
disabled,
|
|
multiselect,
|
|
selected,
|
|
// no longer used
|
|
focusVisible: false,
|
|
active: false
|
|
};
|
|
};
|