Skip to content

Commit

Permalink
Structure and slots for SearchBox, using Input as a slot (#28090)
Browse files Browse the repository at this point in the history
  • Loading branch information
emmayjiang authored Jun 15, 2023
1 parent 7516a6b commit 89f6b7c
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 55 deletions.
14 changes: 10 additions & 4 deletions packages/react-components/react-search/etc/react-search.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
```ts

/// <reference types="react" />

import type { ComponentProps } from '@fluentui/react-utilities';
import type { ComponentState } from '@fluentui/react-utilities';
import type { ForwardRefComponent } from '@fluentui/react-utilities';
import { Input } from '@fluentui/react-input';
import { InputState } from '@fluentui/react-input';
import * as React_2 from 'react';
import type { Slot } from '@fluentui/react-utilities';
import type { SlotClassNames } from '@fluentui/react-utilities';
Expand All @@ -21,18 +25,20 @@ export const SearchBox: ForwardRefComponent<SearchBoxProps>;
export const searchBoxClassNames: SlotClassNames<SearchBoxSlots>;

// @public
export type SearchBoxProps = ComponentProps<SearchBoxSlots> & {};
export type SearchBoxProps = ComponentProps<SearchBoxSlots>;

// @public (undocumented)
export type SearchBoxSlots = {
root: Slot<'div'>;
root: NonNullable<Slot<typeof Input>>;
dismiss?: Slot<'span'>;
contentAfter?: Slot<'span'>;
};

// @public
export type SearchBoxState = ComponentState<SearchBoxSlots>;
export type SearchBoxState = ComponentState<SearchBoxSlots> & Required<Pick<InputState, 'size'>> & Required<Pick<SearchBoxProps, 'disabled'>>;

// @public
export const useSearchBox_unstable: (props: SearchBoxProps, ref: React_2.Ref<HTMLElement>) => SearchBoxState;
export const useSearchBox_unstable: (props: SearchBoxProps, ref: React_2.Ref<HTMLInputElement>) => SearchBoxState;

// @public
export const useSearchBoxStyles_unstable: (state: SearchBoxState) => SearchBoxState;
Expand Down
2 changes: 2 additions & 0 deletions packages/react-components/react-search/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"@fluentui/scripts-tasks": "*"
},
"dependencies": {
"@fluentui/react-icons": "^2.0.203",
"@fluentui/react-input": "^9.4.16",
"@fluentui/react-jsx-runtime": "9.0.0-alpha.6",
"@fluentui/react-theme": "^9.1.8",
"@fluentui/react-utilities": "^9.9.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ describe('SearchBox', () => {
isConformant({
Component: SearchBox,
displayName: 'SearchBox',
primarySlot: 'input',
});

// TODO add more tests here, and create visual regression tests in /apps/vr-tests

it('renders a default state', () => {
const result = render(<SearchBox>Default SearchBox</SearchBox>);
const result = render(<SearchBox />);
expect(result.container).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import type { ComponentProps, ComponentState, Slot } from '@fluentui/react-utilities';
import { Input, InputState } from '@fluentui/react-input';

export type SearchBoxSlots = {
root: Slot<'div'>;
// Root of the component, wrapping the inputs
root: NonNullable<Slot<typeof Input>>;

// Last element in the input, within the input border
dismiss?: Slot<'span'>;

// Element after the input text, within the input border
contentAfter?: Slot<'span'>;
};

/**
* SearchBox Props
*/
export type SearchBoxProps = ComponentProps<SearchBoxSlots> & {};
export type SearchBoxProps = ComponentProps<SearchBoxSlots>;

/**
* State used in rendering SearchBox
*/
export type SearchBoxState = ComponentState<SearchBoxSlots>;
// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from SearchBoxProps.
// & Required<Pick<SearchBoxProps, 'propName'>>
export type SearchBoxState = ComponentState<SearchBoxSlots> &
Required<Pick<InputState, 'size'>> &
Required<Pick<SearchBoxProps, 'disabled'>>;
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,56 @@

exports[`SearchBox renders a default state 1`] = `
<div>
<div
class="fui-SearchBox"
<span
class="fui-Input fui-SearchBox"
>
Default SearchBox
</div>
<span
class="fui-Input__contentBefore"
>
<svg
aria-hidden="true"
class=""
fill="currentColor"
height="1em"
viewBox="0 0 20 20"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.5 3a5.5 5.5 0 0 1 4.23 9.02l4.12 4.13a.5.5 0 0 1-.63.76l-.07-.06-4.13-4.12A5.5 5.5 0 1 1 8.5 3Zm0 1a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9Z"
fill="currentColor"
/>
</svg>
</span>
<input
class="fui-Input__input"
type="search"
value=""
/>
<span
class="fui-Input__contentAfter fui-SearchBox__contentAfter"
>
<span
aria-label="clear"
class="fui-SearchBox__dismiss"
role="button"
>
<svg
aria-hidden="true"
class=""
fill="currentColor"
height="1em"
viewBox="0 0 20 20"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m4.09 4.22.06-.07a.5.5 0 0 1 .63-.06l.07.06L10 9.29l5.15-5.14a.5.5 0 0 1 .63-.06l.07.06c.18.17.2.44.06.63l-.06.07L10.71 10l5.14 5.15c.18.17.2.44.06.63l-.06.07a.5.5 0 0 1-.63.06l-.07-.06L10 10.71l-5.15 5.14a.5.5 0 0 1-.63.06l-.07-.06a.5.5 0 0 1-.06-.63l.06-.07L9.29 10 4.15 4.85a.5.5 0 0 1-.06-.63l.06-.07-.06.07Z"
fill="currentColor"
/>
</svg>
</span>
</span>
</span>
</div>
`;
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/** @jsxRuntime classic */
/** @jsx createElement */
/** @jsxFrag React.Fragment */

import * as React from 'react';
import { createElement } from '@fluentui/react-jsx-runtime';
import { getSlotsNext } from '@fluentui/react-utilities';
import type { SearchBoxState, SearchBoxSlots } from './SearchBox.types';
Expand All @@ -12,5 +14,17 @@ export const renderSearchBox_unstable = (state: SearchBoxState) => {
const { slots, slotProps } = getSlotsNext<SearchBoxSlots>(state);

// TODO Add additional slots in the appropriate place
return <slots.root {...slotProps.root} />;
const rootSlots = {
contentAfter: slots.contentAfter && {
...slotProps.contentAfter,
children: (
<>
{slotProps.contentAfter.children}
{slots.dismiss && <slots.dismiss {...slotProps.dismiss} />}
</>
),
},
};

return <slots.root {...slotProps.root} {...rootSlots} />;
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as React from 'react';
import { mergeCallbacks, resolveShorthand, useControllableState, useEventCallback } from '@fluentui/react-utilities';
import { Input } from '@fluentui/react-input';
import type { SearchBoxProps, SearchBoxState } from './SearchBox.types';
import { DismissRegular, SearchRegular } from '@fluentui/react-icons';

/**
* Create the state required to render SearchBox.
*
* The returned state can be modified with hooks such as useSearchBoxStyles_unstable,
* before being passed to renderSearchBox_unstable.
*
* @param props - props from this instance of SearchBox
* @param ref - reference to root HTMLElement of SearchBox
*/
export const useSearchBox_unstable = (props: SearchBoxProps, ref: React.Ref<HTMLInputElement>): SearchBoxState => {
const { size = 'medium', disabled = false, contentBefore, dismiss, contentAfter, ...inputProps } = props;

const [value, setValue] = useControllableState({
state: props.value,
defaultState: props.defaultValue,
initialState: '',
});

const state: SearchBoxState = {
components: {
root: Input,
dismiss: 'span',
contentAfter: 'span',
},

root: {
ref,
type: 'search',
input: {}, // defining here to have access in styles hook
value,

contentBefore: resolveShorthand(contentBefore, {
defaultProps: {
children: <SearchRegular />,
},
required: true, // TODO need to allow users to remove
}),

...inputProps,

onChange: useEventCallback(ev => {
const newValue = ev.target.value;
props.onChange?.(ev, { value: newValue });
setValue(newValue);
}),
},
dismiss: resolveShorthand(dismiss, {
defaultProps: {
children: <DismissRegular />,
role: 'button',
'aria-label': 'clear',
},
required: true,
}),
contentAfter: resolveShorthand(contentAfter, { required: true }),

disabled,
size,
};

const onDismissClick = useEventCallback(mergeCallbacks(state.dismiss?.onClick, () => setValue('')));
if (state.dismiss) {
state.dismiss.onClick = onDismissClick;
}

return state;
};
Loading

0 comments on commit 89f6b7c

Please sign in to comment.