From e0d813ad78ed897c74fdd8902bdb3c83c0a4c2c1 Mon Sep 17 00:00:00 2001 From: Maxime Cloutier Date: Fri, 4 Aug 2023 14:34:40 -0400 Subject: [PATCH 01/15] add search box on action groups --- .../components/MenuGroup/MenuGroup.tsx | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/polaris-react/src/components/ActionMenu/components/MenuGroup/MenuGroup.tsx b/polaris-react/src/components/ActionMenu/components/MenuGroup/MenuGroup.tsx index 5052ed6e5f4..c8348ec03f9 100644 --- a/polaris-react/src/components/ActionMenu/components/MenuGroup/MenuGroup.tsx +++ b/polaris-react/src/components/ActionMenu/components/MenuGroup/MenuGroup.tsx @@ -1,9 +1,10 @@ -import React, {useCallback} from 'react'; +import React, {useCallback, useState} from 'react'; import type {ActionListSection, MenuGroupDescriptor} from '../../../../types'; import {ActionList} from '../../../ActionList'; import {Popover} from '../../../Popover'; import {SecondaryAction} from '../SecondaryAction'; +import {TextField} from '../../../TextField'; import styles from './MenuGroup.scss'; @@ -38,6 +39,7 @@ export function MenuGroup({ getOffsetWidth, sections, }: MenuGroupProps) { + const [searchText, setSeachText] = useState(''); const handleClose = useCallback(() => { onClose(title); }, [onClose, title]); @@ -75,6 +77,16 @@ export function MenuGroup({ ); + const filteredActions = actions.filter((action) => + action.content?.toLowerCase().includes(searchText.toLowerCase()), + ); + + const filteredSections = sections?.filter((section) => + section.items.some((item) => + item.content?.toLowerCase().includes(searchText.toLowerCase()), + ), + ); + return ( + setSeachText(value)} + /> {details &&
{details}
} From cc5a43563ee3896adadc0455a281abe3101d90bf Mon Sep 17 00:00:00 2001 From: Maxime Cloutier Date: Fri, 4 Aug 2023 14:49:54 -0400 Subject: [PATCH 02/15] add search box on action groups on mobile --- .../RollupActions/RollupActions.tsx | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/polaris-react/src/components/ActionMenu/components/RollupActions/RollupActions.tsx b/polaris-react/src/components/ActionMenu/components/RollupActions/RollupActions.tsx index b89e6ce59ad..5260678e10b 100644 --- a/polaris-react/src/components/ActionMenu/components/RollupActions/RollupActions.tsx +++ b/polaris-react/src/components/ActionMenu/components/RollupActions/RollupActions.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useState} from 'react'; import {HorizontalDotsMinor} from '@shopify/polaris-icons'; import type { @@ -10,6 +10,7 @@ import {useToggle} from '../../../../utilities/use-toggle'; import {ActionList} from '../../../ActionList'; import {Button} from '../../../Button'; import {Popover} from '../../../Popover'; +import {TextField} from '../../../TextField'; import styles from './RollupActions.scss'; @@ -28,6 +29,7 @@ export function RollupActions({ sections = [], }: RollupActionsProps) { const i18n = useI18n(); + const [searchText, setSeachText] = useState(''); const {value: rollupOpen, toggle: toggleRollupOpen} = useToggle(false); @@ -49,6 +51,16 @@ export function RollupActions({ ); + const filteredItems = items.filter((item) => + item.content?.toLowerCase().includes(searchText.toLowerCase()), + ); + + const filteredSections = sections?.filter((section) => + section.items.some((item) => + item.content?.toLowerCase().includes(searchText.toLowerCase()), + ), + ); + return ( + setSeachText(value)} + /> From c4ee485c9da08de326a8afa4d1df717e407503c2 Mon Sep 17 00:00:00 2001 From: Maxime Cloutier Date: Wed, 9 Aug 2023 08:52:20 -0400 Subject: [PATCH 03/15] Update implementation --- polaris-react/playground/DetailsPage.tsx | 18 +++++++ .../src/components/ActionList/ActionList.tsx | 52 ++++++++++++++++--- .../components/MenuGroup/MenuGroup.tsx | 25 ++------- .../RollupActions/RollupActions.tsx | 27 ++-------- .../IndexTable/IndexTable.stories.tsx | 24 +++++++++ 5 files changed, 95 insertions(+), 51 deletions(-) diff --git a/polaris-react/playground/DetailsPage.tsx b/polaris-react/playground/DetailsPage.tsx index 46f54b024b4..f18c1db12bc 100644 --- a/polaris-react/playground/DetailsPage.tsx +++ b/polaris-react/playground/DetailsPage.tsx @@ -587,6 +587,24 @@ export function DetailsPage() { content: 'Toggle page actions', onAction: toggleActions, }, + { + content: 'Embed on a website', + + onAction: () => console.log('embed'), + }, + { + content: 'Toggle page actions', + onAction: toggleActions, + }, + { + content: 'Embed on a website', + + onAction: () => console.log('embed'), + }, + { + content: 'Toggle page actions', + onAction: toggleActions, + }, ], }, ]} diff --git a/polaris-react/src/components/ActionList/ActionList.tsx b/polaris-react/src/components/ActionList/ActionList.tsx index 92ec54bdafc..dac98308964 100644 --- a/polaris-react/src/components/ActionList/ActionList.tsx +++ b/polaris-react/src/components/ActionList/ActionList.tsx @@ -1,16 +1,17 @@ -import React, {useRef} from 'react'; +import React, {useMemo, useRef, useState} from 'react'; +import type {ActionListItemDescriptor, ActionListSection} from '../../types'; +import {Key} from '../../types'; import { wrapFocusNextFocusableMenuItem, wrapFocusPreviousFocusableMenuItem, } from '../../utilities/focus'; -import {KeypressListener} from '../KeypressListener'; -import {Key} from '../../types'; -import type {ActionListItemDescriptor, ActionListSection} from '../../types'; import {Box} from '../Box'; +import {KeypressListener} from '../KeypressListener'; +import {TextField} from '../TextField'; -import {Section, Item} from './components'; import type {ItemProps} from './components'; +import {Item, Section} from './components'; export interface ActionListProps { /** Collection of actions for list */ @@ -33,6 +34,7 @@ export function ActionList({ }: ActionListProps) { let finalSections: readonly ActionListSection[] = []; const actionListRef = useRef(null); + const [searchText, setSeachText] = useState(''); if (items) { finalSections = [{items}, ...sections]; @@ -46,7 +48,14 @@ export function ActionList({ const elementTabIndex = hasMultipleSections && actionRole === 'menuitem' ? -1 : undefined; - const sectionMarkup = finalSections.map((section, index) => { + const filteredSections = finalSections?.map((section) => ({ + ...section, + items: section.items.filter((item) => + item.content?.toLowerCase().includes(searchText.toLowerCase()), + ), + })); + + const sectionMarkup = filteredSections.map((section, index) => { return section.items.length > 0 ? (
) : null; + const totalActions = + finalSections?.reduce( + (acc: number, section) => acc + section.items.length, + 0, + ) || 0; + + const totalFilteredActions = useMemo(() => { + const totalSectionItems = + filteredSections?.reduce( + (acc: number, section) => acc + section.items.length, + 0, + ) || 0; + + return totalSectionItems; + }, [filteredSections]); + + const showSearch = totalActions >= 10; + return ( {listeners} + + {showSearch && ( + 0 ? '0' : '2'}> + setSeachText(value)} + /> + + )} {sectionMarkup} ); diff --git a/polaris-react/src/components/ActionMenu/components/MenuGroup/MenuGroup.tsx b/polaris-react/src/components/ActionMenu/components/MenuGroup/MenuGroup.tsx index c8348ec03f9..5052ed6e5f4 100644 --- a/polaris-react/src/components/ActionMenu/components/MenuGroup/MenuGroup.tsx +++ b/polaris-react/src/components/ActionMenu/components/MenuGroup/MenuGroup.tsx @@ -1,10 +1,9 @@ -import React, {useCallback, useState} from 'react'; +import React, {useCallback} from 'react'; import type {ActionListSection, MenuGroupDescriptor} from '../../../../types'; import {ActionList} from '../../../ActionList'; import {Popover} from '../../../Popover'; import {SecondaryAction} from '../SecondaryAction'; -import {TextField} from '../../../TextField'; import styles from './MenuGroup.scss'; @@ -39,7 +38,6 @@ export function MenuGroup({ getOffsetWidth, sections, }: MenuGroupProps) { - const [searchText, setSeachText] = useState(''); const handleClose = useCallback(() => { onClose(title); }, [onClose, title]); @@ -77,16 +75,6 @@ export function MenuGroup({ ); - const filteredActions = actions.filter((action) => - action.content?.toLowerCase().includes(searchText.toLowerCase()), - ); - - const filteredSections = sections?.filter((section) => - section.items.some((item) => - item.content?.toLowerCase().includes(searchText.toLowerCase()), - ), - ); - return ( - setSeachText(value)} - /> {details &&
{details}
} diff --git a/polaris-react/src/components/ActionMenu/components/RollupActions/RollupActions.tsx b/polaris-react/src/components/ActionMenu/components/RollupActions/RollupActions.tsx index 5260678e10b..7b0664caa67 100644 --- a/polaris-react/src/components/ActionMenu/components/RollupActions/RollupActions.tsx +++ b/polaris-react/src/components/ActionMenu/components/RollupActions/RollupActions.tsx @@ -1,16 +1,15 @@ -import React, {useState} from 'react'; import {HorizontalDotsMinor} from '@shopify/polaris-icons'; +import React from 'react'; import type { - ActionListSection, ActionListItemDescriptor, + ActionListSection, } from '../../../../types'; import {useI18n} from '../../../../utilities/i18n'; import {useToggle} from '../../../../utilities/use-toggle'; import {ActionList} from '../../../ActionList'; import {Button} from '../../../Button'; import {Popover} from '../../../Popover'; -import {TextField} from '../../../TextField'; import styles from './RollupActions.scss'; @@ -29,7 +28,6 @@ export function RollupActions({ sections = [], }: RollupActionsProps) { const i18n = useI18n(); - const [searchText, setSeachText] = useState(''); const {value: rollupOpen, toggle: toggleRollupOpen} = useToggle(false); @@ -51,16 +49,6 @@ export function RollupActions({ ); - const filteredItems = items.filter((item) => - item.content?.toLowerCase().includes(searchText.toLowerCase()), - ); - - const filteredSections = sections?.filter((section) => - section.items.some((item) => - item.content?.toLowerCase().includes(searchText.toLowerCase()), - ), - ); - return ( - setSeachText(value)} - /> diff --git a/polaris-react/src/components/IndexTable/IndexTable.stories.tsx b/polaris-react/src/components/IndexTable/IndexTable.stories.tsx index e170da57088..62e17f01306 100644 --- a/polaris-react/src/components/IndexTable/IndexTable.stories.tsx +++ b/polaris-react/src/components/IndexTable/IndexTable.stories.tsx @@ -898,6 +898,30 @@ export function WithBulkActions() { content: 'Delete customers', onAction: () => console.log('Todo: implement bulk delete'), }, + { + content: 'Add tags', + onAction: () => console.log('Todo: implement bulk add tags'), + }, + { + content: 'Remove tags', + onAction: () => console.log('Todo: implement bulk remove tags'), + }, + { + content: 'Delete customers', + onAction: () => console.log('Todo: implement bulk delete'), + }, + { + content: 'Add tags', + onAction: () => console.log('Todo: implement bulk add tags'), + }, + { + content: 'Remove tags', + onAction: () => console.log('Todo: implement bulk remove tags'), + }, + { + content: 'Delete customers', + onAction: () => console.log('Todo: implement bulk delete'), + }, ]; const rowMarkup = customers.map( From 3353f29be850688a2252a9c4fdfe48ca9dd48ceb Mon Sep 17 00:00:00 2001 From: Maxime Cloutier Date: Wed, 9 Aug 2023 08:54:18 -0400 Subject: [PATCH 04/15] Cleanup --- polaris-react/playground/DetailsPage.tsx | 18 -------------- .../IndexTable/IndexTable.stories.tsx | 24 ------------------- 2 files changed, 42 deletions(-) diff --git a/polaris-react/playground/DetailsPage.tsx b/polaris-react/playground/DetailsPage.tsx index f18c1db12bc..46f54b024b4 100644 --- a/polaris-react/playground/DetailsPage.tsx +++ b/polaris-react/playground/DetailsPage.tsx @@ -587,24 +587,6 @@ export function DetailsPage() { content: 'Toggle page actions', onAction: toggleActions, }, - { - content: 'Embed on a website', - - onAction: () => console.log('embed'), - }, - { - content: 'Toggle page actions', - onAction: toggleActions, - }, - { - content: 'Embed on a website', - - onAction: () => console.log('embed'), - }, - { - content: 'Toggle page actions', - onAction: toggleActions, - }, ], }, ]} diff --git a/polaris-react/src/components/IndexTable/IndexTable.stories.tsx b/polaris-react/src/components/IndexTable/IndexTable.stories.tsx index 62e17f01306..e170da57088 100644 --- a/polaris-react/src/components/IndexTable/IndexTable.stories.tsx +++ b/polaris-react/src/components/IndexTable/IndexTable.stories.tsx @@ -898,30 +898,6 @@ export function WithBulkActions() { content: 'Delete customers', onAction: () => console.log('Todo: implement bulk delete'), }, - { - content: 'Add tags', - onAction: () => console.log('Todo: implement bulk add tags'), - }, - { - content: 'Remove tags', - onAction: () => console.log('Todo: implement bulk remove tags'), - }, - { - content: 'Delete customers', - onAction: () => console.log('Todo: implement bulk delete'), - }, - { - content: 'Add tags', - onAction: () => console.log('Todo: implement bulk add tags'), - }, - { - content: 'Remove tags', - onAction: () => console.log('Todo: implement bulk remove tags'), - }, - { - content: 'Delete customers', - onAction: () => console.log('Todo: implement bulk delete'), - }, ]; const rowMarkup = customers.map( From 42ab0e605aaab75aefe27b1b861f787b4e4440de Mon Sep 17 00:00:00 2001 From: Maxime Cloutier Date: Wed, 9 Aug 2023 08:54:49 -0400 Subject: [PATCH 05/15] Cleanup --- .../RollupActions/RollupActions.tsx | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/polaris-react/src/components/ActionMenu/components/RollupActions/RollupActions.tsx b/polaris-react/src/components/ActionMenu/components/RollupActions/RollupActions.tsx index 7b0664caa67..5260678e10b 100644 --- a/polaris-react/src/components/ActionMenu/components/RollupActions/RollupActions.tsx +++ b/polaris-react/src/components/ActionMenu/components/RollupActions/RollupActions.tsx @@ -1,15 +1,16 @@ +import React, {useState} from 'react'; import {HorizontalDotsMinor} from '@shopify/polaris-icons'; -import React from 'react'; import type { - ActionListItemDescriptor, ActionListSection, + ActionListItemDescriptor, } from '../../../../types'; import {useI18n} from '../../../../utilities/i18n'; import {useToggle} from '../../../../utilities/use-toggle'; import {ActionList} from '../../../ActionList'; import {Button} from '../../../Button'; import {Popover} from '../../../Popover'; +import {TextField} from '../../../TextField'; import styles from './RollupActions.scss'; @@ -28,6 +29,7 @@ export function RollupActions({ sections = [], }: RollupActionsProps) { const i18n = useI18n(); + const [searchText, setSeachText] = useState(''); const {value: rollupOpen, toggle: toggleRollupOpen} = useToggle(false); @@ -49,6 +51,16 @@ export function RollupActions({ ); + const filteredItems = items.filter((item) => + item.content?.toLowerCase().includes(searchText.toLowerCase()), + ); + + const filteredSections = sections?.filter((section) => + section.items.some((item) => + item.content?.toLowerCase().includes(searchText.toLowerCase()), + ), + ); + return ( + setSeachText(value)} + /> From 0ada929b5d86f55516d765d90d6f33ebe38ffaa7 Mon Sep 17 00:00:00 2001 From: Maxime Cloutier Date: Wed, 9 Aug 2023 08:56:02 -0400 Subject: [PATCH 06/15] Cleanup cleanup --- .../RollupActions/RollupActions.tsx | 25 +++---------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/polaris-react/src/components/ActionMenu/components/RollupActions/RollupActions.tsx b/polaris-react/src/components/ActionMenu/components/RollupActions/RollupActions.tsx index 5260678e10b..b89e6ce59ad 100644 --- a/polaris-react/src/components/ActionMenu/components/RollupActions/RollupActions.tsx +++ b/polaris-react/src/components/ActionMenu/components/RollupActions/RollupActions.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React from 'react'; import {HorizontalDotsMinor} from '@shopify/polaris-icons'; import type { @@ -10,7 +10,6 @@ import {useToggle} from '../../../../utilities/use-toggle'; import {ActionList} from '../../../ActionList'; import {Button} from '../../../Button'; import {Popover} from '../../../Popover'; -import {TextField} from '../../../TextField'; import styles from './RollupActions.scss'; @@ -29,7 +28,6 @@ export function RollupActions({ sections = [], }: RollupActionsProps) { const i18n = useI18n(); - const [searchText, setSeachText] = useState(''); const {value: rollupOpen, toggle: toggleRollupOpen} = useToggle(false); @@ -51,16 +49,6 @@ export function RollupActions({ ); - const filteredItems = items.filter((item) => - item.content?.toLowerCase().includes(searchText.toLowerCase()), - ); - - const filteredSections = sections?.filter((section) => - section.items.some((item) => - item.content?.toLowerCase().includes(searchText.toLowerCase()), - ), - ); - return ( - setSeachText(value)} - /> From 50756c095e2c41da5d0faa7e2641f4668964aa33 Mon Sep 17 00:00:00 2001 From: Maxime Cloutier Date: Thu, 10 Aug 2023 15:15:15 -0400 Subject: [PATCH 07/15] implement search field --- polaris-react/locales/en.json | 7 + polaris-react/playground/DetailsPage.tsx | 27 ++ .../src/components/ActionList/ActionList.tsx | 36 +-- .../components/SearchField/SearchField.scss | 236 ++++++++++++++++++ .../components/SearchField/SearchField.tsx | 101 ++++++++ .../components/SearchField/index.ts | 1 + .../SearchField/tests/SearchField.test.tsx | 142 +++++++++++ .../components/ActionList/components/index.ts | 2 + 8 files changed, 535 insertions(+), 17 deletions(-) create mode 100644 polaris-react/src/components/ActionList/components/SearchField/SearchField.scss create mode 100644 polaris-react/src/components/ActionList/components/SearchField/SearchField.tsx create mode 100644 polaris-react/src/components/ActionList/components/SearchField/index.ts create mode 100644 polaris-react/src/components/ActionList/components/SearchField/tests/SearchField.test.tsx diff --git a/polaris-react/locales/en.json b/polaris-react/locales/en.json index 75e21e7060b..1407ae0b7ed 100644 --- a/polaris-react/locales/en.json +++ b/polaris-react/locales/en.json @@ -8,6 +8,13 @@ "rollupButton": "View actions" } }, + "ActionList": { + "SearchField": { + "clearButtonLabel": "Clear", + "search": "Search", + "placeholder": "Search actions" + } + }, "Avatar": { "label": "Avatar", "labelWithInitials": "Avatar with initials {initials}" diff --git a/polaris-react/playground/DetailsPage.tsx b/polaris-react/playground/DetailsPage.tsx index 46f54b024b4..56a98d96a3a 100644 --- a/polaris-react/playground/DetailsPage.tsx +++ b/polaris-react/playground/DetailsPage.tsx @@ -587,6 +587,33 @@ export function DetailsPage() { content: 'Toggle page actions', onAction: toggleActions, }, + { + content: 'Embed on a website', + + onAction: () => console.log('embed'), + }, + { + content: 'Toggle page actions', + onAction: toggleActions, + }, + { + content: 'Embed on a website', + + onAction: () => console.log('embed'), + }, + { + content: 'Toggle page actions', + onAction: toggleActions, + }, + { + content: 'Embed on a website', + + onAction: () => console.log('embed'), + }, + { + content: 'Toggle page actions', + onAction: toggleActions, + }, ], }, ]} diff --git a/polaris-react/src/components/ActionList/ActionList.tsx b/polaris-react/src/components/ActionList/ActionList.tsx index dac98308964..2bb53235294 100644 --- a/polaris-react/src/components/ActionList/ActionList.tsx +++ b/polaris-react/src/components/ActionList/ActionList.tsx @@ -8,10 +8,10 @@ import { } from '../../utilities/focus'; import {Box} from '../Box'; import {KeypressListener} from '../KeypressListener'; -import {TextField} from '../TextField'; +import {useI18n} from '../../utilities/i18n'; +import {SearchField, Item, Section} from './components'; import type {ItemProps} from './components'; -import {Item, Section} from './components'; export interface ActionListProps { /** Collection of actions for list */ @@ -32,6 +32,8 @@ export function ActionList({ actionRole, onActionAnyItem, }: ActionListProps) { + const i18n = useI18n(); + let finalSections: readonly ActionListSection[] = []; const actionListRef = useRef(null); const [searchText, setSeachText] = useState(''); @@ -127,28 +129,28 @@ export function ActionList({ const showSearch = totalActions >= 10; return ( - - {listeners} - + <> {showSearch && ( 0 ? '0' : '2'}> - setSeachText(value)} /> )} - {sectionMarkup} - + + {listeners} + {sectionMarkup} + + ); } diff --git a/polaris-react/src/components/ActionList/components/SearchField/SearchField.scss b/polaris-react/src/components/ActionList/components/SearchField/SearchField.scss new file mode 100644 index 00000000000..e5aa3d7158f --- /dev/null +++ b/polaris-react/src/components/ActionList/components/SearchField/SearchField.scss @@ -0,0 +1,236 @@ +@import '../../../../styles/common'; + +$icon-size: 20px; +$new-input-height: 36px; +$search-icon-width: calc(#{$icon-size} + var(--p-space-4)); + +$icon-size-se23: 18px; +$new-input-height-se23: 32px; +$search-icon-width-se23: calc(#{$icon-size-se23} + var(--p-space-3)); + +.SearchField { + // stylelint-disable -- Polaris component custom properties + --pc-search-field-backdrop: 1; + --pc-search-field-input: 2; + --pc-search-field-icon: 3; + --pc-search-field-action: 3; + // stylelint-enable + z-index: var(--p-z-index-11); + position: relative; + display: flex; + flex: 1 1 auto; + align-items: center; + border: var(--p-border-width-1) solid transparent; + width: 100%; +} +// We have both a focused class and a focus pseudo selector here +// because we allow "faked" focus for when the search is still +// active, but is not actually the focused element in the DOM +// (for example, while selecting a value from a filter in the +// search this input controls) +.focused .Input, +.Input:focus { + border: none; + color: var(--p-color-text); + + #{$se23} & { + color: var(--p-color-text-inverse); + } + + &::placeholder { + color: var(--p-color-text-subdued); + + #{$se23} & { + color: var(--p-color-text-inverse-subdued); + } + } +} + +.Input:focus-visible { + ~ .Backdrop { + // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY + @include focus-ring($style: 'focused'); + } + + ~ .BackdropShowFocusBorder { + // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY + border: var(--p-border-width-1) solid var(--pc-top-bar-border); + } + + // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY + ~ .Icon svg { + fill: var(--p-color-icon); + + #{$se23} & { + fill: var(--p-color-icon-subdued); + } + } +} + +.Input:focus-visible:not(:active) { + #{$se23} & { + // stylelint-disable-next-line -- se23 + ~ .Backdrop { + outline: var(--p-border-width-2) solid + var(--p-color-border-interactive-focus); + outline-offset: var(--p-space-05); + } + + // stylelint-disable-next-line -- se23 + ~ .Icon svg { + fill: var(--p-color-icon-subdued); + } + } +} + +.Input { + // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY + @include text-style-input; + // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY + z-index: var(--pc-search-field-input); + height: $new-input-height; + width: 100%; + padding: 0 0 0 $search-icon-width; + border: none; + background-color: transparent; + outline: none; + color: var(--p-color-text); + will-change: fill, color; + transition: fill var(--p-motion-duration-200) var(--p-motion-ease), + color var(--p-motion-duration-200) var(--p-motion-ease); + + #{$se23} & { + padding: 0 $search-icon-width-se23 0 $search-icon-width-se23; + color: var(--p-color-text-inverse-subdued); + height: $new-input-height-se23; + border: var(--p-border-width-1-experimental) solid + var(--p-color-border-inverse); + border-radius: var(--p-border-radius-2); + + &:hover { + border-color: var(--p-color-border-hover); + } + + &:active, + &:focus { + box-shadow: inset 0 0 0 var(--p-border-width-1) var(--p-color-border); + } + } + + &::placeholder { + color: var(--p-color-text); + + #{$se23} & { + color: var(--p-color-text-inverse-subdued); + } + } + + &::-webkit-search-decoration, + &::-webkit-search-cancel-button { + appearance: none; + } +} + +.Icon { + position: absolute; + // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY + z-index: var(--pc-search-field-icon); + top: 50%; + left: var(--p-space-2); + display: flex; + height: $icon-size; + pointer-events: none; + transform: translateY(-50%); + + #{$se23} & { + height: $icon-size-se23; + width: $icon-size-se23; + } + + svg { + fill: var(--p-color-icon); + + #{$se23} & { + fill: var(--p-color-icon-subdued); + } + } +} + +.Clear { + // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY + @include focus-ring($size: 'wide'); + position: relative; + // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY + z-index: var(--pc-search-field-action); + border: none; + appearance: none; + background: transparent; + padding: var(--p-space-2); + + #{$se23} & { + position: absolute; + right: var(--p-space-1); + padding: var(--p-space-1); + } + + svg { + fill: var(--p-color-icon); + + #{$se23} & { + fill: var(--p-color-icon-subdued); + } + } + + &:focus, + &:hover { + outline: none; + } + + &:hover svg, + &:focus svg { + fill: var(--p-color-icon-hover); + + #{$se23} & { + fill: var(--p-color-icon-inverse); + } + } + + &:focus-visible { + // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY + @include focus-ring($size: 'wide', $style: 'focused'); + } + + &:active { + svg { + fill: var(--p-color-icon-active); + } + + &::after { + border: none; + } + } +} + +.Backdrop { + // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY + @include focus-ring($border-width: 1px); + position: absolute; + // stylelint-disable-next-line -- generated by polaris-migrator DO NOT COPY + z-index: var(--pc-text-field-backdrop); + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: var(--p-color-bg); + border: var(--p-border-width-1) solid var(--p-color-border-input); + // stylelint-disable-next-line -- hard coded to address accessbility issue https://github.com/Shopify/polaris/issues/7838 + border-top-color: #898f94; + border-radius: var(--p-border-radius-1); + pointer-events: none; + + #{$se23} & { + border-radius: var(--p-border-radius-2); + border-width: var(--p-border-width-1-experimental); + background-color: var(--p-color-bg-input); + } +} diff --git a/polaris-react/src/components/ActionList/components/SearchField/SearchField.tsx b/polaris-react/src/components/ActionList/components/SearchField/SearchField.tsx new file mode 100644 index 00000000000..ae6b85e56cd --- /dev/null +++ b/polaris-react/src/components/ActionList/components/SearchField/SearchField.tsx @@ -0,0 +1,101 @@ +import {CircleCancelMinor, SearchMinor} from '@shopify/polaris-icons'; +import React, {useCallback, useId, useRef} from 'react'; + +import {useI18n} from '../../../../utilities/i18n'; +import {Icon} from '../../../Icon'; +import {Text} from '../../../Text'; + +import styles from './SearchField.scss'; + +export interface SearchFieldProps { + /** Initial value for the input */ + value: string; + /** Hint text to display */ + placeholder?: string; + /** Callback when value is changed */ + onChange(value: string): void; +} + +export function SearchField({value, placeholder, onChange}: SearchFieldProps) { + const i18n = useI18n(); + + const input = useRef(null); + const searchId = useId(); + + const handleChange = useCallback( + ({currentTarget}: React.ChangeEvent) => { + onChange(currentTarget.value); + }, + [onChange], + ); + + const handleClear = useCallback(() => { + if (!input.current) { + return; + } + + input.current.value = ''; + onChange(''); + input.current.focus(); + }, [onChange]); + + const clearMarkup = value !== '' && ( + + ); + + const handleRef = (ref: HTMLInputElement) => { + input.current = ref; + + // It won't focus if it's on the same tick as when it renders + setTimeout(() => { + ref?.focus(); + }); + }; + + return ( +
+ + + + + + + + + {clearMarkup} +
+
+ ); +} + +function preventDefault(event: React.KeyboardEvent) { + if (event.key === 'Enter') { + event.preventDefault(); + } +} diff --git a/polaris-react/src/components/ActionList/components/SearchField/index.ts b/polaris-react/src/components/ActionList/components/SearchField/index.ts new file mode 100644 index 00000000000..9f5ef334723 --- /dev/null +++ b/polaris-react/src/components/ActionList/components/SearchField/index.ts @@ -0,0 +1 @@ +export * from './SearchField'; diff --git a/polaris-react/src/components/ActionList/components/SearchField/tests/SearchField.test.tsx b/polaris-react/src/components/ActionList/components/SearchField/tests/SearchField.test.tsx new file mode 100644 index 00000000000..fb74b82425f --- /dev/null +++ b/polaris-react/src/components/ActionList/components/SearchField/tests/SearchField.test.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import {CircleCancelMinor} from '@shopify/polaris-icons'; +import {mountWithApp} from 'tests/utilities'; + +import {Icon} from '../../../../Icon'; +import {SearchField} from '../SearchField'; + +describe('', () => { + it('passes the placeholder prop to input', () => { + const textField = mountWithApp( + , + ); + + expect(textField).toContainReactComponent('input', { + placeholder: 'hello polaris', + }); + }); + + describe('focused', () => { + it('will give input focus when the focused prop is true', () => { + const textField = mountWithApp( + , + ); + + expect(document.activeElement).toBe(textField.find('input')!.domNode); + }); + + it('will give input focus if focus has been toggled', () => { + const textField = mountWithApp( + , + ); + + expect(document.activeElement).not.toBe(textField.find('input')!.domNode); + + textField.setProps({value: '', onChange: noop, focused: true}); + expect(document.activeElement).toBe(textField.find('input')!.domNode); + }); + + it('will blur input if focused has been toggled', () => { + const textField = mountWithApp( + , + ); + + textField.setProps({value: '', onChange: noop, focused: false}); + expect(document.activeElement).not.toBe(textField.find('input')!.domNode); + }); + }); + + describe('clear content', () => { + it('will render a cancel icon when a value is provided', () => { + const textField = mountWithApp( + , + ); + + expect(textField).toContainReactComponent(Icon, { + source: CircleCancelMinor, + }); + }); + + it('will call the onChange with an empty string when the cancel button is pressed', () => { + const spy = jest.fn(); + const textField = mountWithApp( + , + ); + + textField.find('button')!.trigger('onClick'); + + expect(spy).toHaveBeenCalledWith(''); + }); + }); + + describe('onBlur()', () => { + it('is called when the text field is blurred', () => { + const spy = jest.fn(); + const textField = mountWithApp( + , + ); + + textField.find('div')!.trigger('onBlur'); + + expect(spy).toHaveBeenCalledTimes(1); + }); + }); + + describe('onFocus', () => { + it('is called when the text field is focused', () => { + const spy = jest.fn(); + const textField = mountWithApp( + , + ); + + textField.find('div')!.trigger('onFocus'); + + expect(spy).toHaveBeenCalledTimes(1); + }); + }); + + describe('onChange()', () => { + it('is called with the new value', () => { + const spy = jest.fn(); + const newValue = 'hello polaris'; + const textField = mountWithApp( + , + ); + + textField.find('input')!.trigger('onChange', { + currentTarget: { + value: newValue, + }, + }); + + expect(spy).toHaveBeenCalledWith(newValue); + }); + }); + + describe('onKeyDown', () => { + it("will prevent default on the 'enter' keydown", () => { + const spy = jest.fn(); + const textField = mountWithApp( + , + ); + + textField.find('input')!.trigger('onKeyDown', { + key: 'Enter', + preventDefault: spy, + }); + expect(spy).toHaveBeenCalled(); + }); + }); + + it('adds a "BackdropShowFocusBorder" class when "showFocusBorder" is passed', () => { + const textField = mountWithApp( + , + ); + + expect(textField).toContainReactComponent('div', { + className: 'Backdrop BackdropShowFocusBorder', + }); + }); +}); + +function noop() {} diff --git a/polaris-react/src/components/ActionList/components/index.ts b/polaris-react/src/components/ActionList/components/index.ts index 1f6e50145f3..edefe0dc331 100644 --- a/polaris-react/src/components/ActionList/components/index.ts +++ b/polaris-react/src/components/ActionList/components/index.ts @@ -1,3 +1,5 @@ export * from './Item'; export * from './Section'; + +export * from './SearchField'; From c326e8fe445b93ec051a1c867d709682e3a607ed Mon Sep 17 00:00:00 2001 From: Maxime Cloutier Date: Thu, 10 Aug 2023 15:19:29 -0400 Subject: [PATCH 08/15] fix tests --- .../SearchField/tests/SearchField.test.tsx | 66 ------------------- 1 file changed, 66 deletions(-) diff --git a/polaris-react/src/components/ActionList/components/SearchField/tests/SearchField.test.tsx b/polaris-react/src/components/ActionList/components/SearchField/tests/SearchField.test.tsx index fb74b82425f..a9142825ed2 100644 --- a/polaris-react/src/components/ActionList/components/SearchField/tests/SearchField.test.tsx +++ b/polaris-react/src/components/ActionList/components/SearchField/tests/SearchField.test.tsx @@ -16,36 +16,6 @@ describe('', () => { }); }); - describe('focused', () => { - it('will give input focus when the focused prop is true', () => { - const textField = mountWithApp( - , - ); - - expect(document.activeElement).toBe(textField.find('input')!.domNode); - }); - - it('will give input focus if focus has been toggled', () => { - const textField = mountWithApp( - , - ); - - expect(document.activeElement).not.toBe(textField.find('input')!.domNode); - - textField.setProps({value: '', onChange: noop, focused: true}); - expect(document.activeElement).toBe(textField.find('input')!.domNode); - }); - - it('will blur input if focused has been toggled', () => { - const textField = mountWithApp( - , - ); - - textField.setProps({value: '', onChange: noop, focused: false}); - expect(document.activeElement).not.toBe(textField.find('input')!.domNode); - }); - }); - describe('clear content', () => { it('will render a cancel icon when a value is provided', () => { const textField = mountWithApp( @@ -69,32 +39,6 @@ describe('', () => { }); }); - describe('onBlur()', () => { - it('is called when the text field is blurred', () => { - const spy = jest.fn(); - const textField = mountWithApp( - , - ); - - textField.find('div')!.trigger('onBlur'); - - expect(spy).toHaveBeenCalledTimes(1); - }); - }); - - describe('onFocus', () => { - it('is called when the text field is focused', () => { - const spy = jest.fn(); - const textField = mountWithApp( - , - ); - - textField.find('div')!.trigger('onFocus'); - - expect(spy).toHaveBeenCalledTimes(1); - }); - }); - describe('onChange()', () => { it('is called with the new value', () => { const spy = jest.fn(); @@ -127,16 +71,6 @@ describe('', () => { expect(spy).toHaveBeenCalled(); }); }); - - it('adds a "BackdropShowFocusBorder" class when "showFocusBorder" is passed', () => { - const textField = mountWithApp( - , - ); - - expect(textField).toContainReactComponent('div', { - className: 'Backdrop BackdropShowFocusBorder', - }); - }); }); function noop() {} From f4461ec851cc0c3bf401bab78ae117de6aa2470e Mon Sep 17 00:00:00 2001 From: Maxime Cloutier Date: Fri, 11 Aug 2023 09:56:14 -0400 Subject: [PATCH 09/15] Add tests --- .../ActionList/tests/ActionList.test.tsx | 108 +++++++++++++++++- 1 file changed, 107 insertions(+), 1 deletion(-) diff --git a/polaris-react/src/components/ActionList/tests/ActionList.test.tsx b/polaris-react/src/components/ActionList/tests/ActionList.test.tsx index 14ed6e9cab5..fd51bc20961 100644 --- a/polaris-react/src/components/ActionList/tests/ActionList.test.tsx +++ b/polaris-react/src/components/ActionList/tests/ActionList.test.tsx @@ -4,7 +4,7 @@ import {mountWithApp} from 'tests/utilities'; import {ActionList} from '../ActionList'; import {Badge} from '../../Badge'; -import {Item, Section} from '../components'; +import {Item, SearchField, Section} from '../components'; import {Key} from '../../../types'; import {KeypressListener} from '../../KeypressListener'; @@ -236,4 +236,110 @@ describe('', () => { actionListButtons[actionListButtons.length - 1].domNode, ); }); + + it('does not render search with 9 or less items', () => { + const actionList = mountWithApp( + , + ); + + expect(actionList).not.toContainReactComponentTimes(SearchField, 1); + }); + + it('renders search with 10 or more items', () => { + const actionList = mountWithApp( + , + ); + + expect(actionList).toContainReactComponentTimes(SearchField, 1); + }); + + it('renders search with 10 or more items or section items', () => { + const actionList = mountWithApp( + , + ); + + expect(actionList).toContainReactComponentTimes(SearchField, 1); + }); + + it('filters items and section items with case-insensitive search', () => { + const actionList = mountWithApp( + , + ); + + const textField = actionList.find('input'); + textField!.trigger('onChange', { + currentTarget: { + value: 'item 1', + }, + }); + + expect(actionList).toContainReactComponentTimes(Item, 2); + // First Section will have no title since items without a section are grouped into a Section automatically + expect(actionList.findAll(Section)[1]).toContainReactText('Section 2'); + }); }); From 0ec5cda43a8314a324cc17137f147201044f49a6 Mon Sep 17 00:00:00 2001 From: Maxime Cloutier Date: Fri, 11 Aug 2023 09:59:01 -0400 Subject: [PATCH 10/15] cleanup --- polaris-react/playground/DetailsPage.tsx | 28 ------------------------ 1 file changed, 28 deletions(-) diff --git a/polaris-react/playground/DetailsPage.tsx b/polaris-react/playground/DetailsPage.tsx index 56a98d96a3a..a310fc2425b 100644 --- a/polaris-react/playground/DetailsPage.tsx +++ b/polaris-react/playground/DetailsPage.tsx @@ -580,34 +580,6 @@ export function DetailsPage() { actions: [ { content: 'Embed on a website', - - onAction: () => console.log('embed'), - }, - { - content: 'Toggle page actions', - onAction: toggleActions, - }, - { - content: 'Embed on a website', - - onAction: () => console.log('embed'), - }, - { - content: 'Toggle page actions', - onAction: toggleActions, - }, - { - content: 'Embed on a website', - - onAction: () => console.log('embed'), - }, - { - content: 'Toggle page actions', - onAction: toggleActions, - }, - { - content: 'Embed on a website', - onAction: () => console.log('embed'), }, { From 879494e15e65eea842d146230939cb612cf88117 Mon Sep 17 00:00:00 2001 From: Maxime Cloutier Date: Fri, 11 Aug 2023 10:02:59 -0400 Subject: [PATCH 11/15] changeset --- .changeset/curly-chairs-speak.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/curly-chairs-speak.md diff --git a/.changeset/curly-chairs-speak.md b/.changeset/curly-chairs-speak.md new file mode 100644 index 00000000000..9d0858b1a78 --- /dev/null +++ b/.changeset/curly-chairs-speak.md @@ -0,0 +1,5 @@ +--- +'@shopify/polaris': minor +--- + +Add a search field to filter ActionList that have more than 10 items From bcedf452e9423138777dd503e9677c276dcde3e8 Mon Sep 17 00:00:00 2001 From: Maxime Cloutier Date: Wed, 16 Aug 2023 09:55:15 -0400 Subject: [PATCH 12/15] Fix placholder --- polaris-react/src/components/ActionList/ActionList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polaris-react/src/components/ActionList/ActionList.tsx b/polaris-react/src/components/ActionList/ActionList.tsx index 2bb53235294..88e959ccef4 100644 --- a/polaris-react/src/components/ActionList/ActionList.tsx +++ b/polaris-react/src/components/ActionList/ActionList.tsx @@ -134,7 +134,7 @@ export function ActionList({ 0 ? '0' : '2'}> setSeachText(value)} From 5f1091677a4898bdd0674aff0c70870a39ca5a15 Mon Sep 17 00:00:00 2001 From: Maxime Cloutier Date: Wed, 16 Aug 2023 09:56:16 -0400 Subject: [PATCH 13/15] Adjust amount of items needed for filter --- polaris-react/src/components/ActionList/ActionList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/polaris-react/src/components/ActionList/ActionList.tsx b/polaris-react/src/components/ActionList/ActionList.tsx index 88e959ccef4..07286654dd2 100644 --- a/polaris-react/src/components/ActionList/ActionList.tsx +++ b/polaris-react/src/components/ActionList/ActionList.tsx @@ -126,7 +126,7 @@ export function ActionList({ return totalSectionItems; }, [filteredSections]); - const showSearch = totalActions >= 10; + const showSearch = totalActions >= 8; return ( <> From deb201d56deb6176f460ca445ba500dbc412a101 Mon Sep 17 00:00:00 2001 From: Maxime Cloutier Date: Wed, 16 Aug 2023 09:58:08 -0400 Subject: [PATCH 14/15] Fix tests --- .../src/components/ActionList/tests/ActionList.test.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/polaris-react/src/components/ActionList/tests/ActionList.test.tsx b/polaris-react/src/components/ActionList/tests/ActionList.test.tsx index fd51bc20961..ed10230353e 100644 --- a/polaris-react/src/components/ActionList/tests/ActionList.test.tsx +++ b/polaris-react/src/components/ActionList/tests/ActionList.test.tsx @@ -237,7 +237,7 @@ describe('', () => { ); }); - it('does not render search with 9 or less items', () => { + it('does not render search with 7 or less items', () => { const actionList = mountWithApp( ', () => { {content: 'Item 5'}, {content: 'Item 6'}, {content: 'Item 7'}, - {content: 'Item 8'}, - {content: 'Item 9'}, ]} />, ); @@ -257,7 +255,7 @@ describe('', () => { expect(actionList).not.toContainReactComponentTimes(SearchField, 1); }); - it('renders search with 10 or more items', () => { + it('renders search with 8 or more items', () => { const actionList = mountWithApp( Date: Thu, 17 Aug 2023 10:24:01 +0000 Subject: [PATCH 15/15] Update 6 translation files --- polaris-react/locales/de.json | 7 +++++++ polaris-react/locales/es.json | 7 +++++++ polaris-react/locales/nb.json | 7 +++++++ polaris-react/locales/pl.json | 7 +++++++ polaris-react/locales/pt-BR.json | 7 +++++++ polaris-react/locales/pt-PT.json | 7 +++++++ 6 files changed, 42 insertions(+) diff --git a/polaris-react/locales/de.json b/polaris-react/locales/de.json index 97576fc65e1..3c851469765 100644 --- a/polaris-react/locales/de.json +++ b/polaris-react/locales/de.json @@ -391,6 +391,13 @@ "cancel": "Abbrechen" } } + }, + "ActionList": { + "SearchField": { + "clearButtonLabel": "Löschen", + "search": "Suchen", + "placeholder": "Aktionen durchsuchen" + } } } } diff --git a/polaris-react/locales/es.json b/polaris-react/locales/es.json index e972c9a8567..f935bee6394 100644 --- a/polaris-react/locales/es.json +++ b/polaris-react/locales/es.json @@ -392,6 +392,13 @@ "cancel": "Cancelar" } } + }, + "ActionList": { + "SearchField": { + "clearButtonLabel": "Borrar", + "search": "Búsqueda", + "placeholder": "Buscar acciones" + } } } } diff --git a/polaris-react/locales/nb.json b/polaris-react/locales/nb.json index 5e34e39ab4e..2aeb7202b08 100644 --- a/polaris-react/locales/nb.json +++ b/polaris-react/locales/nb.json @@ -391,6 +391,13 @@ "cancel": "Avbryt" } } + }, + "ActionList": { + "SearchField": { + "clearButtonLabel": "Tøm", + "search": "Søk", + "placeholder": "Søk i handlinger" + } } } } diff --git a/polaris-react/locales/pl.json b/polaris-react/locales/pl.json index b62aad8ae65..606ad1b00a0 100644 --- a/polaris-react/locales/pl.json +++ b/polaris-react/locales/pl.json @@ -393,6 +393,13 @@ "cancel": "Anuluj" } } + }, + "ActionList": { + "SearchField": { + "clearButtonLabel": "Wyczyść", + "search": "Szukaj", + "placeholder": "Szukaj czynności" + } } } } diff --git a/polaris-react/locales/pt-BR.json b/polaris-react/locales/pt-BR.json index 0fa87854f96..663a1d02c5e 100644 --- a/polaris-react/locales/pt-BR.json +++ b/polaris-react/locales/pt-BR.json @@ -392,6 +392,13 @@ "cancel": "Cancelar" } } + }, + "ActionList": { + "SearchField": { + "clearButtonLabel": "Limpar", + "search": "Pesquisar", + "placeholder": "Pesquisar ações" + } } } } diff --git a/polaris-react/locales/pt-PT.json b/polaris-react/locales/pt-PT.json index a8f2caf1efb..0217e2b3843 100644 --- a/polaris-react/locales/pt-PT.json +++ b/polaris-react/locales/pt-PT.json @@ -392,6 +392,13 @@ "cancel": "Cancelar" } } + }, + "ActionList": { + "SearchField": { + "clearButtonLabel": "Limpar", + "search": "Pesquisar", + "placeholder": "Pesquisar ações" + } } } }