forked from Shopify/polaris
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
### WHY are these changes introduced? Closes: https://github.com/Shopify/polaris-internal/issues/1510 <!-- Context about the problem that’s being addressed. --> ### WHAT is this pull request doing? Introduces a new picker, undocumented and prefixed with Alpha until it's better tested in the admin. Only single select / filtering needs to be working for the first release <!-- Summary of the changes committed. Before / after screenshots are appreciated for UI changes. Make sure to include alt text that describes the screenshot. Include a video if your changes include interactive content. If you include an animated gif showing your change, wrapping it in a details tag is recommended. Gifs usually autoplay, which can cause accessibility issues for people reviewing your PR: <details> <summary>Summary of your gif(s)</summary> <img src="..." alt="Description of what the gif shows"> </details> --> ### How to 🎩 Change / add vendors [here](https://admin.web.web-17ze.kyle-durand.us.spin.dev/store/shop1/products/5) ### 🎩 checklist - [x] Tested a [snapshot](https://github.com/Shopify/polaris/blob/main/documentation/Releasing.md#-snapshot-releases) - [ ] Tested on [mobile](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting.md#cross-browser-testing) - [x] Tested on [multiple browsers](https://help.shopify.com/en/manual/shopify-admin/supported-browsers) - [x] Tested for [accessibility](https://github.com/Shopify/polaris/blob/main/documentation/Accessibility%20testing.md) - [ ] Updated the component's `README.md` with documentation changes - [ ] [Tophatted documentation](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting%20documentation.md) changes in the style guide
- Loading branch information
1 parent
a6f4b9d
commit cf25021
Showing
20 changed files
with
695 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@shopify/polaris': minor | ||
--- | ||
|
||
Added new AlphaPicker component |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,7 @@ import { | |
TopBar, | ||
FooterHelp, | ||
Link, | ||
AlphaPicker, | ||
} from '../src'; | ||
import type {DropZoneProps, PageProps} from '../src'; | ||
|
||
|
@@ -54,6 +55,11 @@ export function DetailsPage() { | |
emailFieldValue: '[email protected]', | ||
nameFieldValue: 'Jaded Pixel', | ||
}); | ||
const [query, setQuery] = useState(''); | ||
const [vendors, setVendors] = useState([ | ||
{value: 'The North Face', children: 'The North Face'}, | ||
{value: 'Patagonia', children: 'Patagonia'}, | ||
]); | ||
const skipToContentRef = useRef<HTMLAnchorElement>(null); | ||
const [toastActive, setToastActive] = useState(false); | ||
const [isLoading, setIsLoading] = useState(false); | ||
|
@@ -79,6 +85,16 @@ export function DetailsPage() { | |
const [supportSubject, setSupportSubject] = useState(''); | ||
const [supportMessage, setSupportMessage] = useState(''); | ||
|
||
const handleSelect = (selected: string) => { | ||
setQuery(''); | ||
if (vendors.some((vendor) => vendor.children === selected)) return; | ||
|
||
setVendors((vendors) => [ | ||
...vendors, | ||
{value: selected, children: selected}, | ||
]); | ||
}; | ||
|
||
const handleDiscard = useCallback(() => { | ||
setEmailFieldValue(defaultState.current.emailFieldValue); | ||
setNameFieldValue(defaultState.current.nameFieldValue); | ||
|
@@ -635,11 +651,24 @@ export function DetailsPage() { | |
value={selected} | ||
/> | ||
<br /> | ||
<Select | ||
label="Vendor" | ||
options={options} | ||
onChange={setSelected} | ||
value={selected} | ||
<AlphaPicker | ||
onSelect={handleSelect} | ||
activator={{ | ||
label: 'Vendor', | ||
placeholder: 'None selected', | ||
}} | ||
searchField={{ | ||
label: 'Search vendors', | ||
placeholder: 'Search or add new vendor', | ||
autoComplete: 'off', | ||
value: query, | ||
onChange: (value) => setQuery(value), | ||
}} | ||
options={vendors} | ||
addAction={{ | ||
value: query, | ||
children: `Add ${query}`, | ||
}} | ||
/> | ||
</LegacyCard.Section> | ||
<LegacyCard.Section title="Collections" /> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from './Listbox'; | ||
export * from './components'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,258 @@ | ||
import React, {useState, useMemo, useCallback, isValidElement} from 'react'; | ||
import {SearchIcon} from '@shopify/polaris-icons'; | ||
|
||
import {Popover} from '../Popover'; | ||
import { | ||
ComboboxTextFieldContext, | ||
ComboboxListboxContext, | ||
ComboboxListboxOptionContext, | ||
} from '../../utilities/combobox'; | ||
import type { | ||
ComboboxTextFieldType, | ||
ComboboxListboxType, | ||
ComboboxListboxOptionType, | ||
} from '../../utilities/combobox'; | ||
import {Box} from '../Box'; | ||
import type {TextFieldProps} from '../TextField'; | ||
import type {ListboxProps, OptionProps} from '../Listbox'; | ||
import {Listbox} from '../Listbox'; | ||
import type {IconProps} from '../Icon'; | ||
import {Icon} from '../Icon'; | ||
|
||
import {Activator, SearchField} from './components'; | ||
import type {ActivatorProps} from './components'; | ||
|
||
export interface PickerProps extends Omit<ListboxProps, 'children'> { | ||
/** Configure the button that activates the Picker */ | ||
activator: ActivatorProps; | ||
/** Allows more than one option to be selected */ | ||
allowMultiple?: boolean; | ||
/** The options to be listed within the picker */ | ||
options?: OptionProps[]; | ||
/** Used to add a new picker option that isn't listed */ | ||
addAction?: OptionProps & {icon?: IconProps['source']}; | ||
/** TextField that allows filtering of options */ | ||
searchField?: TextFieldProps; | ||
/** Whether or not more options are available to lazy load when the bottom of the listbox reached. Use the hasMoreResults boolean provided by the GraphQL API of the paginated data. */ | ||
willLoadMoreOptions?: boolean; | ||
/** Height to set on the Popover Pane. */ | ||
height?: string; | ||
/** Callback fired when the bottom of the listbox is reached. Use to lazy load when listbox option data is paginated. */ | ||
onScrolledToBottom?(): void; | ||
/** Callback fired when the popover closes */ | ||
onClose?(): void; | ||
} | ||
|
||
const FILTER_REGEX = (value: string) => new RegExp(value, 'i'); | ||
const QUERY_REGEX = (value: string) => new RegExp(`^${value}$`, 'i'); | ||
|
||
export function Picker({ | ||
activator, | ||
allowMultiple, | ||
searchField, | ||
options = [], | ||
willLoadMoreOptions, | ||
height, | ||
addAction, | ||
onScrolledToBottom, | ||
onClose, | ||
...listboxProps | ||
}: PickerProps) { | ||
const activatorRef = React.createRef<HTMLButtonElement>(); | ||
const [activeItem, setActiveItem] = useState<string>(); | ||
const [popoverActive, setPopoverActive] = useState(false); | ||
const [activeOptionId, setActiveOptionId] = useState<string>(); | ||
const [textFieldLabelId, setTextFieldLabelId] = useState<string>(); | ||
const [listboxId, setListboxId] = useState<string>(); | ||
const [query, setQuery] = useState<string>(''); | ||
const [filteredOptions, setFilteredOptions] = useState<OptionProps[] | null>( | ||
options, | ||
); | ||
|
||
const shouldOpen = !popoverActive; | ||
const handleClose = useCallback(() => { | ||
setPopoverActive(false); | ||
onClose?.(); | ||
activatorRef.current?.focus(); | ||
}, [activatorRef, onClose]); | ||
|
||
const handleOpen = useCallback(() => { | ||
setPopoverActive(true); | ||
}, []); | ||
|
||
const handleFocus = useCallback(() => { | ||
if (shouldOpen) handleOpen(); | ||
}, [shouldOpen, handleOpen]); | ||
|
||
const handleChange = useCallback(() => { | ||
if (shouldOpen) handleOpen(); | ||
}, [shouldOpen, handleOpen]); | ||
|
||
const handleBlur = useCallback(() => { | ||
if (popoverActive) { | ||
handleClose(); | ||
setQuery(''); | ||
setFilteredOptions(options); | ||
} | ||
}, [popoverActive, handleClose, options]); | ||
|
||
const textFieldContextValue: ComboboxTextFieldType = useMemo( | ||
() => ({ | ||
activeOptionId, | ||
expanded: popoverActive, | ||
listboxId, | ||
setTextFieldLabelId, | ||
onTextFieldFocus: handleFocus, | ||
onTextFieldChange: handleChange, | ||
onTextFieldBlur: handleBlur, | ||
}), | ||
[ | ||
activeOptionId, | ||
popoverActive, | ||
listboxId, | ||
setTextFieldLabelId, | ||
handleFocus, | ||
handleChange, | ||
handleBlur, | ||
], | ||
); | ||
|
||
const listboxOptionContextValue: ComboboxListboxOptionType = useMemo( | ||
() => ({ | ||
allowMultiple, | ||
}), | ||
[allowMultiple], | ||
); | ||
|
||
const listboxContextValue: ComboboxListboxType = useMemo( | ||
() => ({ | ||
listboxId, | ||
textFieldLabelId, | ||
textFieldFocused: popoverActive, | ||
willLoadMoreOptions, | ||
setActiveOptionId, | ||
setListboxId, | ||
onKeyToBottom: onScrolledToBottom, | ||
}), | ||
[ | ||
listboxId, | ||
textFieldLabelId, | ||
popoverActive, | ||
willLoadMoreOptions, | ||
setActiveOptionId, | ||
setListboxId, | ||
onScrolledToBottom, | ||
], | ||
); | ||
|
||
const updateText = useCallback( | ||
(value: string) => { | ||
setQuery(value); | ||
|
||
if (value === '') { | ||
setFilteredOptions(options); | ||
return; | ||
} | ||
|
||
const resultOptions = options?.filter((option) => | ||
FILTER_REGEX(value).exec(reactChildrenText(option.children)), | ||
); | ||
setFilteredOptions(resultOptions ?? []); | ||
}, | ||
[options], | ||
); | ||
|
||
const firstSelectedOption = reactChildrenText( | ||
options.find((option) => option.value === activeItem)?.children, | ||
); | ||
|
||
const queryMatchesExistingOption = options.some((option) => | ||
QUERY_REGEX(query).exec(reactChildrenText(option.children)), | ||
); | ||
|
||
return ( | ||
<Popover | ||
activator={ | ||
<Activator | ||
{...activator} | ||
onClick={handleOpen} | ||
selected={firstSelectedOption || ''} | ||
placeholder={activator.placeholder} | ||
ref={activatorRef} | ||
/> | ||
} | ||
active={popoverActive} | ||
autofocusTarget="none" | ||
onClose={handleClose} | ||
preferredPosition="cover" | ||
preventFocusOnClose | ||
> | ||
<Popover.Pane onScrolledToBottom={onScrolledToBottom} height={height}> | ||
{searchField ? ( | ||
<Box | ||
paddingBlockStart="200" | ||
paddingBlockEnd="100" | ||
paddingInline="200" | ||
borderBlockEndWidth="025" | ||
borderColor="border" | ||
> | ||
<ComboboxTextFieldContext.Provider value={textFieldContextValue}> | ||
<SearchField | ||
{...searchField} | ||
value={query} | ||
onChange={(value) => { | ||
updateText(value); | ||
searchField.onChange?.(value, searchField.id ?? ''); | ||
}} | ||
prefix={<Icon source={SearchIcon} />} | ||
labelHidden | ||
focused={popoverActive} | ||
/> | ||
</ComboboxTextFieldContext.Provider> | ||
</Box> | ||
) : null} | ||
|
||
<ComboboxListboxContext.Provider value={listboxContextValue}> | ||
<ComboboxListboxOptionContext.Provider | ||
value={listboxOptionContextValue} | ||
> | ||
<Box paddingBlock="200"> | ||
<Listbox | ||
{...listboxProps} | ||
onSelect={(selected: string) => { | ||
setQuery(''); | ||
updateText(''); | ||
setActiveItem(selected); | ||
listboxProps.onSelect?.(selected); | ||
|
||
if (!allowMultiple) { | ||
handleClose(); | ||
} | ||
}} | ||
> | ||
{filteredOptions?.map((option) => ( | ||
<Listbox.Option | ||
key={option.value} | ||
selected={option.value === activeItem} | ||
{...option} | ||
/> | ||
))} | ||
{addAction && query !== '' && !queryMatchesExistingOption ? ( | ||
<Listbox.Action {...addAction} value={query} /> | ||
) : null} | ||
</Listbox> | ||
</Box> | ||
</ComboboxListboxOptionContext.Provider> | ||
</ComboboxListboxContext.Provider> | ||
</Popover.Pane> | ||
</Popover> | ||
); | ||
} | ||
|
||
const reactChildrenText = (children: React.ReactNode): string => { | ||
if (typeof children === 'string') return children; | ||
|
||
return isValidElement(children) | ||
? reactChildrenText(children?.props?.children) | ||
: ''; | ||
}; |
31 changes: 31 additions & 0 deletions
31
polaris-react/src/components/Picker/components/Activator/Activator.module.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
.Activator { | ||
background: none; | ||
outline: none; | ||
padding: var(--p-space-200) var(--p-space-300); | ||
border-radius: var(--p-border-radius-200); | ||
border: var(--p-border-width-025) solid var(--p-color-border); | ||
width: 100%; | ||
display: flex; | ||
align-items: center; | ||
justify-content: space-between; | ||
cursor: pointer; | ||
|
||
&:hover { | ||
background-color: var(--p-color-bg-surface-hover); | ||
} | ||
|
||
&:active { | ||
background-color: var(--p-color-bg-surface-active); | ||
} | ||
|
||
&:focus:not(:active) { | ||
outline: var(--p-border-width-050) solid var(--p-color-border-focus); | ||
outline-offset: var(--p-space-025); | ||
} | ||
} | ||
|
||
.disabled { | ||
pointer-events: none; | ||
background-color: var(--p-color-bg-surface-disabled); | ||
border-color: var(--p-color-border-disabled); | ||
} |
Oops, something went wrong.