Skip to content

Commit

Permalink
Updated pickers with input attribute compatibility. (#33243)
Browse files Browse the repository at this point in the history
Co-authored-by: Makoto Morimoto <Humberto.Morimoto@microsoft.com>
  • Loading branch information
tpalacino and khmakoto authored Dec 12, 2024
1 parent 1175ab2 commit 53dd771
Show file tree
Hide file tree
Showing 10 changed files with 316 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "fix: Added label, required, and error properties to BasePicker.",
"packageName": "@fluentui/react",
"email": "tpalacino@users.noreply.github.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import { TextField } from '@fluentui/react/lib/TextField';
import { Checkbox } from '@fluentui/react/lib/Checkbox';
import { IPersonaProps, IPersonaStyles } from '@fluentui/react/lib/Persona';
import {
Expand Down Expand Up @@ -42,6 +43,9 @@ const personaStyles: Partial<IPersonaStyles> = {
export const PeoplePickerListExample: React.FunctionComponent = () => {
const [delayResults, setDelayResults] = React.useState(false);
const [isPickerDisabled, setIsPickerDisabled] = React.useState(false);
const [pickerLabel, setPickerLabel] = React.useState<string | undefined>('Choose People');
const [showPickerLabel, setShowPickerLabel] = React.useState(false);
const [isPickerRequired, setIsPickerRequired] = React.useState(false);
const [mostRecentlyUsed, setMostRecentlyUsed] = React.useState<IPersonaProps[]>(mru);
const [peopleList, setPeopleList] = React.useState<IPersonaProps[]>(people);

Expand Down Expand Up @@ -102,6 +106,18 @@ export const PeoplePickerListExample: React.FunctionComponent = () => {
setIsPickerDisabled(!isPickerDisabled);
};

const onShowLabelButtonClick = (): void => {
setShowPickerLabel(!showPickerLabel);
};

const onPickerLabelChange = (_: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string): void => {
setPickerLabel(newValue ?? '');
};

const onRequiredButtonClick = (): void => {
setIsPickerRequired(!isPickerRequired);
};

const onToggleDelayResultsChange = (): void => {
setDelayResults(!delayResults);
};
Expand All @@ -115,9 +131,17 @@ export const PeoplePickerListExample: React.FunctionComponent = () => {
);
};

const onGetErrorMessage = React.useCallback(
(items: IPersonaProps[]): string | JSX.Element | PromiseLike<string | JSX.Element> | undefined => {
return isPickerRequired && (items || []).length === 0 ? 'Please fill out this field.' : undefined;
},
[isPickerRequired],
);

return (
<div>
<ListPeoplePicker
label={showPickerLabel ? pickerLabel : undefined}
// eslint-disable-next-line react/jsx-no-bind
onResolveSuggestions={onFilterChanged}
// eslint-disable-next-line react/jsx-no-bind
Expand All @@ -141,6 +165,8 @@ export const PeoplePickerListExample: React.FunctionComponent = () => {
componentRef={picker}
resolveDelay={300}
disabled={isPickerDisabled}
required={isPickerRequired}
onGetErrorMessage={onGetErrorMessage}
/>
<Checkbox
label="Disable People Picker"
Expand All @@ -156,6 +182,28 @@ export const PeoplePickerListExample: React.FunctionComponent = () => {
onChange={onToggleDelayResultsChange}
styles={checkboxStyles}
/>
<Checkbox
label="Required People Picker"
checked={isPickerRequired}
// eslint-disable-next-line react/jsx-no-bind
onChange={onRequiredButtonClick}
styles={checkboxStyles}
/>
<Checkbox
label="Show Label"
checked={showPickerLabel}
// eslint-disable-next-line react/jsx-no-bind
onChange={onShowLabelButtonClick}
styles={checkboxStyles}
/>
{showPickerLabel && (
<TextField
label={'People Picker Label'}
value={pickerLabel}
// eslint-disable-next-line react/jsx-no-bind
onChange={onPickerLabelChange}
/>
)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import { TextField } from '@fluentui/react/lib/TextField';
import { Checkbox } from '@fluentui/react/lib/Checkbox';
import { IPersonaProps } from '@fluentui/react/lib/Persona';
import {
Expand Down Expand Up @@ -29,6 +30,9 @@ const checkboxStyles = {
export const PeoplePickerNormalExample: React.FunctionComponent = () => {
const [delayResults, setDelayResults] = React.useState(false);
const [isPickerDisabled, setIsPickerDisabled] = React.useState(false);
const [pickerLabel, setPickerLabel] = React.useState<string | undefined>('Choose People');
const [showPickerLabel, setShowPickerLabel] = React.useState(false);
const [isPickerRequired, setIsPickerRequired] = React.useState(false);
const [showSecondaryText, setShowSecondaryText] = React.useState(false);
const [mostRecentlyUsed, setMostRecentlyUsed] = React.useState<IPersonaProps[]>(mru);
const [peopleList, setPeopleList] = React.useState<IPersonaProps[]>(people);
Expand Down Expand Up @@ -103,6 +107,18 @@ export const PeoplePickerNormalExample: React.FunctionComponent = () => {
setIsPickerDisabled(!isPickerDisabled);
};

const onShowLabelButtonClick = (): void => {
setShowPickerLabel(!showPickerLabel);
};

const onPickerLabelChange = (_: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string): void => {
setPickerLabel(newValue ?? '');
};

const onRequiredButtonClick = (): void => {
setIsPickerRequired(!isPickerRequired);
};

const onToggleDelayResultsChange = (): void => {
setDelayResults(!delayResults);
};
Expand All @@ -111,9 +127,17 @@ export const PeoplePickerNormalExample: React.FunctionComponent = () => {
setShowSecondaryText(!showSecondaryText);
};

const onGetErrorMessage = React.useCallback(
(items: IPersonaProps[]): string | JSX.Element | PromiseLike<string | JSX.Element> | undefined => {
return isPickerRequired && (items || []).length === 0 ? 'Please fill out this field.' : undefined;
},
[isPickerRequired],
);

return (
<div>
<NormalPeoplePicker
label={showPickerLabel ? pickerLabel : undefined}
// eslint-disable-next-line react/jsx-no-bind
onResolveSuggestions={onFilterChanged}
// eslint-disable-next-line react/jsx-no-bind
Expand All @@ -137,6 +161,8 @@ export const PeoplePickerNormalExample: React.FunctionComponent = () => {
onInputChange={onInputChange}
resolveDelay={300}
disabled={isPickerDisabled}
required={isPickerRequired}
onGetErrorMessage={onGetErrorMessage}
/>
<Checkbox
label="Disable People Picker"
Expand All @@ -159,6 +185,28 @@ export const PeoplePickerNormalExample: React.FunctionComponent = () => {
onChange={onToggleShowSecondaryText}
styles={checkboxStyles}
/>
<Checkbox
label="Required People Picker"
checked={isPickerRequired}
// eslint-disable-next-line react/jsx-no-bind
onChange={onRequiredButtonClick}
styles={checkboxStyles}
/>
<Checkbox
label="Show Label"
checked={showPickerLabel}
// eslint-disable-next-line react/jsx-no-bind
onChange={onShowLabelButtonClick}
styles={checkboxStyles}
/>
{showPickerLabel && (
<TextField
label={'People Picker Label'}
value={pickerLabel}
// eslint-disable-next-line react/jsx-no-bind
onChange={onPickerLabelChange}
/>
)}
</div>
);
};
Expand Down
17 changes: 17 additions & 0 deletions packages/react/etc/react.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,8 @@ export class BasePicker<T extends {}, P extends IBasePickerProps<T>> extends Rea
static getDerivedStateFromProps(newProps: IBasePickerProps<any>): {
items: any[];
} | null;
// (undocumented)
protected _getDescribedBy: (items: T[], hasError: boolean) => string;
// @deprecated (undocumented)
protected getSuggestionsAlert(suggestionAlertClassName?: string): JSX.Element | undefined;
// (undocumented)
Expand Down Expand Up @@ -764,8 +766,12 @@ export class BasePicker<T extends {}, P extends IBasePickerProps<T>> extends Rea
// (undocumented)
protected renderCustomAlert(alertClassName?: string): JSX.Element;
// (undocumented)
protected renderError(className?: string): JSX.Element | null;
// (undocumented)
protected renderItems(): JSX.Element[];
// (undocumented)
protected renderLabel(inputId: string, styles: IStyleFunctionOrObject<ILabelStyleProps, ILabelStyles> | undefined): JSX.Element | null;
// (undocumented)
protected renderSuggestions(): JSX.Element | null;
// (undocumented)
protected resetFocus(index?: number): void;
Expand Down Expand Up @@ -2382,9 +2388,11 @@ export interface IBasePickerProps<T> extends IReactProps<any> {
defaultSelectedItems?: T[];
disabled?: boolean;
enableSelectedSuggestionAlert?: boolean;
errorMessage?: string | JSX.Element;
getTextFromItem?: (item: T, currentValue?: string) => string;
inputProps?: IInputProps;
itemLimit?: number;
label?: string;
onBlur?: React_2.FocusEventHandler<HTMLInputElement | Autofill>;
onChange?: (items?: T[]) => void;
onDismiss?: (ev?: any, selectedItem?: T) => boolean | void;
Expand All @@ -2393,6 +2401,7 @@ export interface IBasePickerProps<T> extends IReactProps<any> {
onEmptyResolveSuggestions?: (selectedItems?: T[]) => T[] | PromiseLike<T[]>;
// @deprecated
onFocus?: React_2.FocusEventHandler<HTMLInputElement | Autofill>;
onGetErrorMessage?: (items: T[]) => string | JSX.Element | PromiseLike<string | JSX.Element> | undefined;
onGetMoreResults?: (filter: string, selectedItems?: T[]) => T[] | PromiseLike<T[]>;
onInputChange?: (input: string) => string;
onItemSelected?: (selectedItem?: T) => T | PromiseLike<T> | null;
Expand All @@ -2405,6 +2414,7 @@ export interface IBasePickerProps<T> extends IReactProps<any> {
pickerSuggestionsProps?: IBasePickerSuggestionsProps;
removeButtonAriaLabel?: string;
removeButtonIconProps?: IIconProps;
required?: boolean;
resolveDelay?: number;
searchingText?: ((props: {
input: string;
Expand All @@ -2419,6 +2429,8 @@ export interface IBasePickerProps<T> extends IReactProps<any> {

// @public (undocumented)
export interface IBasePickerState<T> {
// (undocumented)
errorMessage?: string | JSX.Element;
// (undocumented)
isFocused?: boolean;
// (undocumented)
Expand Down Expand Up @@ -2447,16 +2459,20 @@ export interface IBasePickerState<T> {

// @public
export type IBasePickerStyleProps = Pick<IBasePickerProps<any>, 'theme' | 'className' | 'disabled'> & {
hasErrorMessage: boolean;
isFocused?: boolean;
inputClassName?: string;
};

// @public
export interface IBasePickerStyles {
error: IStyle;
input: IStyle;
itemsWrapper: IStyle;
root: IStyle;
screenReaderText: IStyle;
// Warning: (ae-forgotten-export) The symbol "IBasePickerSubComponentStyles" needs to be exported by the entry point index.d.ts
subComponentStyles: IBasePickerSubComponentStyles;
text: IStyle;
}

Expand Down Expand Up @@ -7700,6 +7716,7 @@ export type IPickerAriaIds = {
selectedItems: string;
suggestionList: string;
combobox: string;
error: string;
};

// @public
Expand Down
31 changes: 29 additions & 2 deletions packages/react/src/components/pickers/BasePicker.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import type { IStyle } from '../../Styling';

const GlobalClassNames = {
root: 'ms-BasePicker',
label: 'ms-BasePicker-label',
text: 'ms-BasePicker-text',
itemsWrapper: 'ms-BasePicker-itemsWrapper',
input: 'ms-BasePicker-input',
error: 'ms-BasePicker-error',
};

export function getStyles(props: IBasePickerStyleProps): IBasePickerStyles {
const { className, theme, isFocused, inputClassName, disabled } = props;
const { className, theme, isFocused, inputClassName, disabled, hasErrorMessage } = props;

if (!theme) {
throw new Error('theme is undefined or null in base BasePicker getStyles function.');
Expand Down Expand Up @@ -56,8 +58,22 @@ export function getStyles(props: IBasePickerStyleProps): IBasePickerStyles {
// const disabledOverlayColor = rgbColor ? `rgba(${rgbColor.r}, ${rgbColor.g}, ${rgbColor.b}, 0.29)` : 'transparent';
const disabledOverlayColor = 'rgba(218, 218, 218, 0.29)';

const focusColor = isFocused && !disabled && (hasErrorMessage ? semanticColors.errorText : inputFocusBorderAlt);

return {
root: [classNames.root, className, { position: 'relative' }],
error: [
classNames.error,
{
fontSize: 12,
fontWeight: 400,
color: semanticColors.errorText,
margin: 0,
paddingTop: 5,
display: hasErrorMessage ? 'flex' : 'none',
alignItems: 'center',
},
],
text: [
classNames.text,
{
Expand All @@ -79,7 +95,7 @@ export function getStyles(props: IBasePickerStyleProps): IBasePickerStyles {
},
},
},
isFocused && !disabled && getInputFocusStyle(inputFocusBorderAlt, effects.roundedCorner2),
focusColor && getInputFocusStyle(focusColor, effects.roundedCorner2),
disabled && {
borderColor: disabledOverlayColor,
selectors: {
Expand All @@ -102,6 +118,14 @@ export function getStyles(props: IBasePickerStyleProps): IBasePickerStyles {
},
},
},
hasErrorMessage && {
borderColor: semanticColors.errorText,
selectors: {
':hover': {
borderColor: semanticColors.errorText,
},
},
},
],
itemsWrapper: [
classNames.itemsWrapper,
Expand Down Expand Up @@ -138,5 +162,8 @@ export function getStyles(props: IBasePickerStyleProps): IBasePickerStyles {
inputClassName,
],
screenReaderText: hiddenContentStyle,
subComponentStyles: {
label: {},
},
};
}
Loading

0 comments on commit 53dd771

Please sign in to comment.