From 89f6b7c0835aca7ce29aff3c9a22ace4eb1b01d5 Mon Sep 17 00:00:00 2001
From: Emma Jiang <31319479+emmayjiang@users.noreply.github.com>
Date: Thu, 15 Jun 2023 14:53:09 -0700
Subject: [PATCH] Structure and slots for SearchBox, using Input as a slot
(#28090)
---
.../react-search/etc/react-search.api.md | 14 ++-
.../react-search/package.json | 2 +
.../components/SearchBox/SearchBox.test.tsx | 3 +-
.../components/SearchBox/SearchBox.types.ts | 18 ++-
.../__snapshots__/SearchBox.test.tsx.snap | 54 +++++++-
.../components/SearchBox/renderSearchBox.tsx | 16 ++-
.../src/components/SearchBox/useSearchBox.ts | 28 -----
.../src/components/SearchBox/useSearchBox.tsx | 73 +++++++++++
.../SearchBox/useSearchBoxStyles.styles.ts | 116 ++++++++++++++++--
.../SearchBox/SearchBoxDefault.stories.tsx | 4 +-
10 files changed, 273 insertions(+), 55 deletions(-)
delete mode 100644 packages/react-components/react-search/src/components/SearchBox/useSearchBox.ts
create mode 100644 packages/react-components/react-search/src/components/SearchBox/useSearchBox.tsx
diff --git a/packages/react-components/react-search/etc/react-search.api.md b/packages/react-components/react-search/etc/react-search.api.md
index 64fce5ae0e171a..35e030a97c5b08 100644
--- a/packages/react-components/react-search/etc/react-search.api.md
+++ b/packages/react-components/react-search/etc/react-search.api.md
@@ -4,9 +4,13 @@
```ts
+///
+
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';
@@ -21,18 +25,20 @@ export const SearchBox: ForwardRefComponent;
export const searchBoxClassNames: SlotClassNames;
// @public
-export type SearchBoxProps = ComponentProps & {};
+export type SearchBoxProps = ComponentProps;
// @public (undocumented)
export type SearchBoxSlots = {
- root: Slot<'div'>;
+ root: NonNullable>;
+ dismiss?: Slot<'span'>;
+ contentAfter?: Slot<'span'>;
};
// @public
-export type SearchBoxState = ComponentState;
+export type SearchBoxState = ComponentState & Required> & Required>;
// @public
-export const useSearchBox_unstable: (props: SearchBoxProps, ref: React_2.Ref) => SearchBoxState;
+export const useSearchBox_unstable: (props: SearchBoxProps, ref: React_2.Ref) => SearchBoxState;
// @public
export const useSearchBoxStyles_unstable: (state: SearchBoxState) => SearchBoxState;
diff --git a/packages/react-components/react-search/package.json b/packages/react-components/react-search/package.json
index dfd82bd9f7778e..2bc8da226d1d67 100644
--- a/packages/react-components/react-search/package.json
+++ b/packages/react-components/react-search/package.json
@@ -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",
diff --git a/packages/react-components/react-search/src/components/SearchBox/SearchBox.test.tsx b/packages/react-components/react-search/src/components/SearchBox/SearchBox.test.tsx
index 02785e11cfd379..cd06bc56cd110a 100644
--- a/packages/react-components/react-search/src/components/SearchBox/SearchBox.test.tsx
+++ b/packages/react-components/react-search/src/components/SearchBox/SearchBox.test.tsx
@@ -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(Default SearchBox);
+ const result = render();
expect(result.container).toMatchSnapshot();
});
});
diff --git a/packages/react-components/react-search/src/components/SearchBox/SearchBox.types.ts b/packages/react-components/react-search/src/components/SearchBox/SearchBox.types.ts
index ede74dbf5447a1..9716052f4899cc 100644
--- a/packages/react-components/react-search/src/components/SearchBox/SearchBox.types.ts
+++ b/packages/react-components/react-search/src/components/SearchBox/SearchBox.types.ts
@@ -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>;
+
+ // 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 & {};
+export type SearchBoxProps = ComponentProps;
/**
* State used in rendering SearchBox
*/
-export type SearchBoxState = ComponentState;
-// TODO: Remove semicolon from previous line, uncomment next line, and provide union of props to pick from SearchBoxProps.
-// & Required>
+export type SearchBoxState = ComponentState &
+ Required> &
+ Required>;
diff --git a/packages/react-components/react-search/src/components/SearchBox/__snapshots__/SearchBox.test.tsx.snap b/packages/react-components/react-search/src/components/SearchBox/__snapshots__/SearchBox.test.tsx.snap
index 50af2699391d50..ff04dd5806b3ff 100644
--- a/packages/react-components/react-search/src/components/SearchBox/__snapshots__/SearchBox.test.tsx.snap
+++ b/packages/react-components/react-search/src/components/SearchBox/__snapshots__/SearchBox.test.tsx.snap
@@ -2,10 +2,56 @@
exports[`SearchBox renders a default state 1`] = `
-
- Default SearchBox
-
+
+
+
+
+
+
+
+
+
+
`;
diff --git a/packages/react-components/react-search/src/components/SearchBox/renderSearchBox.tsx b/packages/react-components/react-search/src/components/SearchBox/renderSearchBox.tsx
index 4979cf2c8e5340..1b4f7d4ee899fd 100644
--- a/packages/react-components/react-search/src/components/SearchBox/renderSearchBox.tsx
+++ b/packages/react-components/react-search/src/components/SearchBox/renderSearchBox.tsx
@@ -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';
@@ -12,5 +14,17 @@ export const renderSearchBox_unstable = (state: SearchBoxState) => {
const { slots, slotProps } = getSlotsNext(state);
// TODO Add additional slots in the appropriate place
- return ;
+ const rootSlots = {
+ contentAfter: slots.contentAfter && {
+ ...slotProps.contentAfter,
+ children: (
+ <>
+ {slotProps.contentAfter.children}
+ {slots.dismiss && }
+ >
+ ),
+ },
+ };
+
+ return ;
};
diff --git a/packages/react-components/react-search/src/components/SearchBox/useSearchBox.ts b/packages/react-components/react-search/src/components/SearchBox/useSearchBox.ts
deleted file mode 100644
index 398df621098749..00000000000000
--- a/packages/react-components/react-search/src/components/SearchBox/useSearchBox.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import * as React from 'react';
-import { getNativeElementProps } from '@fluentui/react-utilities';
-import type { SearchBoxProps, SearchBoxState } from './SearchBox.types';
-
-/**
- * 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): SearchBoxState => {
- return {
- // TODO add appropriate props/defaults
- components: {
- // TODO add each slot's element type or component
- root: 'div',
- },
- // TODO add appropriate slots, for example:
- // mySlot: resolveShorthand(props.mySlot),
- root: getNativeElementProps('div', {
- ref,
- ...props,
- }),
- };
-};
diff --git a/packages/react-components/react-search/src/components/SearchBox/useSearchBox.tsx b/packages/react-components/react-search/src/components/SearchBox/useSearchBox.tsx
new file mode 100644
index 00000000000000..788018286e9097
--- /dev/null
+++ b/packages/react-components/react-search/src/components/SearchBox/useSearchBox.tsx
@@ -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): 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: ,
+ },
+ 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: ,
+ 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;
+};
diff --git a/packages/react-components/react-search/src/components/SearchBox/useSearchBoxStyles.styles.ts b/packages/react-components/react-search/src/components/SearchBox/useSearchBoxStyles.styles.ts
index 21fd6fbea8f5d9..e7817f6ee7610c 100644
--- a/packages/react-components/react-search/src/components/SearchBox/useSearchBoxStyles.styles.ts
+++ b/packages/react-components/react-search/src/components/SearchBox/useSearchBoxStyles.styles.ts
@@ -1,33 +1,127 @@
-import { makeStyles, mergeClasses } from '@griffel/react';
+import { makeResetStyles, makeStyles, mergeClasses } from '@griffel/react';
import type { SearchBoxSlots, SearchBoxState } from './SearchBox.types';
import type { SlotClassNames } from '@fluentui/react-utilities';
+import { tokens } from '@fluentui/react-theme';
export const searchBoxClassNames: SlotClassNames = {
root: 'fui-SearchBox',
- // TODO: add class names for all slots on SearchBoxSlots.
- // Should be of the form `: 'fui-SearchBox__`
+ dismiss: 'fui-SearchBox__dismiss',
+ contentAfter: 'fui-SearchBox__contentAfter',
};
/**
* Styles for the root slot
*/
-const useStyles = makeStyles({
- root: {
- // TODO Add default styles for the root element
+const useRootStyles = makeStyles({
+ small: {
+ columnGap: 0,
+ width: '468px',
+
+ paddingLeft: tokens.spacingHorizontalSNudge,
+ paddingRight: tokens.spacingHorizontalSNudge,
+ },
+ medium: {
+ columnGap: 0,
+ width: '468px',
+
+ paddingLeft: tokens.spacingHorizontalS,
+ paddingRight: tokens.spacingHorizontalS,
+ },
+ large: {
+ columnGap: 0,
+ width: '468px',
+
+ paddingLeft: tokens.spacingHorizontalMNudge,
+ paddingRight: tokens.spacingHorizontalMNudge,
+ },
+
+ input: {
+ paddingLeft: tokens.spacingHorizontalSNudge,
+ paddingRight: 0,
+
+ // dismiss + contentAfter appear on focus
+ '& + span': {
+ display: 'none',
+ },
+ '&:focus + span': {
+ display: 'flex',
+ },
+
+ // removes the WebKit pseudoelement styling
+ '::-webkit-search-decoration': {
+ display: 'none',
+ },
+ '::-webkit-search-cancel-button': {
+ display: 'none',
+ },
+ },
+});
+
+const useContentAfterStyles = makeStyles({
+ contentAfter: {
+ paddingLeft: tokens.spacingHorizontalM,
+ columnGap: tokens.spacingHorizontalXS,
},
+});
- // TODO add additional classes for different states and/or slots
+const useDismissClassName = makeResetStyles({
+ boxSizing: 'border-box',
+ color: tokens.colorNeutralForeground3, // "icon color" in design spec
+ display: 'flex',
+ // special case styling for icons (most common case) to ensure they're centered vertically
+ // size: medium (default)
+ '> svg': { fontSize: '20px' },
+});
+
+const useDismissStyles = makeStyles({
+ disabled: {
+ color: tokens.colorNeutralForegroundDisabled,
+ },
+ // Ensure resizable icons show up with the proper font size
+ small: {
+ '> svg': { fontSize: '16px' },
+ },
+ medium: {
+ // included in useDismissClassName
+ },
+ large: {
+ '> svg': { fontSize: '24px' },
+ },
});
/**
* Apply styling to the SearchBox slots based on the state
*/
export const useSearchBoxStyles_unstable = (state: SearchBoxState): SearchBoxState => {
- const styles = useStyles();
- state.root.className = mergeClasses(searchBoxClassNames.root, styles.root, state.root.className);
+ const { disabled, size } = state;
+
+ const rootStyles = useRootStyles();
+ const contentAfterStyles = useContentAfterStyles();
+ const dismissClassName = useDismissClassName();
+ const dismissStyles = useDismissStyles();
+
+ state.root.className = mergeClasses(searchBoxClassNames.root, rootStyles[size], state.root.className);
+ state.root.input!.className = rootStyles.input;
+
+ if (state.dismiss) {
+ state.dismiss.className = mergeClasses(
+ searchBoxClassNames.dismiss,
+ dismissClassName,
+ disabled && dismissStyles.disabled,
+ dismissStyles[size],
+ state.dismiss.className,
+ );
+ }
- // TODO Add class names to slots, for example:
- // state.mySlot.className = mergeClasses(styles.mySlot, state.mySlot.className);
+ if (state.contentAfter) {
+ state.contentAfter!.className = mergeClasses(
+ searchBoxClassNames.contentAfter,
+ contentAfterStyles.contentAfter,
+ state.contentAfter.className,
+ );
+ } else if (state.dismiss) {
+ state.dismiss.className = mergeClasses(state.dismiss.className, contentAfterStyles.contentAfter);
+ }
return state;
};
diff --git a/packages/react-components/react-search/stories/SearchBox/SearchBoxDefault.stories.tsx b/packages/react-components/react-search/stories/SearchBox/SearchBoxDefault.stories.tsx
index 08a6b5018fd0c9..fe7ab84e788d32 100644
--- a/packages/react-components/react-search/stories/SearchBox/SearchBoxDefault.stories.tsx
+++ b/packages/react-components/react-search/stories/SearchBox/SearchBoxDefault.stories.tsx
@@ -1,4 +1,6 @@
import * as React from 'react';
import { SearchBox, SearchBoxProps } from '@fluentui/react-search';
-export const Default = (props: Partial) => ;
+import { FilterRegular } from '@fluentui/react-icons';
+
+export const Default = (props: Partial) => } />;