-
Notifications
You must be signed in to change notification settings - Fork 43
fix: [DHIS2-21708] Org unit selector is expanded when form open if no org unit in top bar #4629
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
786eaf9
33fc96e
0cffa0d
eaabf13
739fa65
18cec80
e4ce807
3a8162f
ac8918c
06a03e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,9 @@ | ||
| import * as React from 'react'; | ||
| import i18n from '@dhis2/d2-i18n'; | ||
| import { debounce } from 'lodash'; | ||
| import { v4 as uuid } from 'uuid'; | ||
| import { Chip, Popover, IconChevronDown16, colors } from '@dhis2/ui'; | ||
| import { withStyles, type WithStyles } from 'capture-core-utils/styles'; | ||
| import { Chip, colors } from '@dhis2/ui'; | ||
| import { OrgUnitField } from './OrgUnitField.component'; | ||
| import { TooltipOrgUnit } from '../../../../Tooltips/TooltipOrgUnit/TooltipOrgUnit.component'; | ||
|
|
||
|
|
@@ -15,6 +18,57 @@ const getStyles = () => ({ | |
| backgroundColor: `${colors.grey200} !important`, | ||
| }, | ||
| }, | ||
| trigger: { | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| boxSizing: 'border-box' as const, | ||
| minHeight: 40, | ||
| padding: '6px 12px', | ||
| border: `1px solid ${colors.grey500}`, | ||
| borderRadius: 3, | ||
| backgroundColor: 'white', | ||
| boxShadow: 'inset 0 0 1px 0 rgba(48, 54, 60, 0.1)', | ||
| cursor: 'pointer', | ||
| '&:focus': { | ||
| outline: 'none', | ||
| borderColor: colors.blue600, | ||
| boxShadow: `inset 0 0 0 2px ${colors.blue600}`, | ||
| }, | ||
| }, | ||
| triggerOpen: { | ||
| borderColor: colors.blue600, | ||
| boxShadow: `inset 0 0 0 2px ${colors.blue600}`, | ||
| }, | ||
| triggerDisabled: { | ||
| backgroundColor: colors.grey100, | ||
| borderColor: colors.grey500, | ||
| color: colors.grey600, | ||
| cursor: 'not-allowed', | ||
| }, | ||
| searchInput: { | ||
| flexGrow: 1, | ||
| minWidth: 0, | ||
| border: 'none', | ||
| outline: 'none', | ||
| background: 'transparent', | ||
| padding: 0, | ||
| fontSize: 14, | ||
| lineHeight: '16px', | ||
| color: colors.grey900, | ||
| cursor: 'inherit', | ||
| '&::placeholder': { | ||
| color: colors.grey600, | ||
| opacity: 1, | ||
| }, | ||
| }, | ||
| chevron: { | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| marginLeft: 'auto', | ||
| }, | ||
| popoverContent: { | ||
| width: 400, | ||
| }, | ||
| }); | ||
|
|
||
| type OrgUnitValue = { | ||
|
|
@@ -25,22 +79,59 @@ type OrgUnitValue = { | |
|
|
||
| type SingleOrgUnitSelectFieldState = { | ||
| previousOrgUnitId: string | null; | ||
| open: boolean; | ||
| inputValue: string; | ||
| searchText: string; | ||
| }; | ||
|
|
||
| type SingleOrgUnitSelectFieldProps = { | ||
| value?: OrgUnitValue; | ||
| onBlur: (value: any) => void; | ||
| onSelectClick?: (orgUnit: Record<string, any>) => void; | ||
| disabled?: boolean; | ||
| maxTreeHeight?: number; | ||
| }; | ||
|
|
||
| type Props = SingleOrgUnitSelectFieldProps & WithStyles<typeof getStyles>; | ||
|
|
||
| class SingleOrgUnitSelectFieldPlain extends React.Component<Props, SingleOrgUnitSelectFieldState> { | ||
| anchorRef: React.RefObject<HTMLDivElement>; | ||
| searchInputRef: React.RefObject<HTMLInputElement>; | ||
| popoverId: string; | ||
| debouncedSetSearchText: ((searchText: string) => void) & { cancel: () => void }; | ||
|
|
||
| constructor(props: Props) { | ||
| super(props); | ||
| this.state = { | ||
| previousOrgUnitId: null, | ||
| open: false, | ||
| inputValue: '', | ||
| searchText: '', | ||
| }; | ||
| this.anchorRef = React.createRef() as React.RefObject<HTMLDivElement>; | ||
| this.searchInputRef = React.createRef() as React.RefObject<HTMLInputElement>; | ||
| this.popoverId = `org-unit-selector-popover-${uuid()}`; | ||
| this.debouncedSetSearchText = debounce((searchText: string) => { | ||
| this.setState({ searchText }); | ||
| }, 300); | ||
| } | ||
|
|
||
| componentWillUnmount() { | ||
| this.debouncedSetSearchText.cancel(); | ||
| } | ||
|
|
||
| openMenu = () => { | ||
| if (this.props.disabled) { | ||
| return; | ||
| } | ||
| this.setState({ open: true }, () => { | ||
| this.searchInputRef.current?.focus(); | ||
| }); | ||
| } | ||
|
|
||
| closeMenu = () => { | ||
| this.debouncedSetSearchText.cancel(); | ||
| this.setState({ open: false, inputValue: '', searchText: '' }); | ||
| } | ||
|
|
||
| onSelectOrgUnit = (orgUnit: Record<string, any>) => { | ||
|
|
@@ -56,6 +147,33 @@ class SingleOrgUnitSelectFieldPlain extends React.Component<Props, SingleOrgUnit | |
| this.props.onBlur(null); | ||
| } | ||
|
|
||
| handleSelect = (orgUnit: Record<string, any>) => { | ||
| if (this.props.onSelectClick) { | ||
| this.props.onSelectClick(orgUnit); | ||
| } else { | ||
| this.onSelectOrgUnit(orgUnit); | ||
| } | ||
| this.closeMenu(); | ||
| } | ||
|
|
||
| handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => { | ||
| const inputValue = event.currentTarget.value; | ||
| this.setState({ inputValue }); | ||
| this.debouncedSetSearchText(inputValue); | ||
| } | ||
|
|
||
| handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => { | ||
| if (this.props.disabled) { | ||
| return; | ||
| } | ||
| if (this.state.open && (event.key === 'Escape' || event.key === 'Tab')) { | ||
| this.closeMenu(); | ||
| } else if (!this.state.open && (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown')) { | ||
| event.preventDefault(); | ||
| this.openMenu(); | ||
| } | ||
| } | ||
|
|
||
| renderSelectedOrgUnit = (selectedOrgUnit: OrgUnitValue) => { | ||
| const { classes } = this.props; | ||
| return ( | ||
|
|
@@ -70,20 +188,73 @@ class SingleOrgUnitSelectFieldPlain extends React.Component<Props, SingleOrgUnit | |
| ); | ||
| } | ||
|
|
||
| renderOrgUnitField = () => { | ||
| const { classes, ...passOnProps } = this.props; | ||
| renderPopover = () => { | ||
| const { classes, value, onBlur, onSelectClick, disabled, maxTreeHeight, ...passOnProps } = this.props; | ||
| return ( | ||
| <Popover | ||
| reference={this.anchorRef.current || undefined} | ||
| arrow={false} | ||
| placement="bottom-start" | ||
| onClickOutside={this.closeMenu} | ||
| maxWidth={400} | ||
| > | ||
| <div id={this.popoverId} className={classes.popoverContent}> | ||
| <OrgUnitField | ||
| {...passOnProps} | ||
| hideSearchField | ||
| searchText={this.state.searchText} | ||
| disabled={disabled} | ||
| maxTreeHeight={maxTreeHeight ?? 350} | ||
| onSelectClick={this.handleSelect} | ||
| onBlur={() => undefined} | ||
| previousOrgUnitId={this.state.previousOrgUnitId} | ||
| /> | ||
| </div> | ||
| </Popover> | ||
| ); | ||
| } | ||
|
|
||
| renderCollapsedOrgUnitField = () => { | ||
| const { classes, disabled } = this.props; | ||
| const { open, inputValue } = this.state; | ||
| const triggerClassName = [ | ||
| classes.trigger, | ||
| open && classes.triggerOpen, | ||
| disabled && classes.triggerDisabled, | ||
| ].filter(Boolean).join(' '); | ||
|
|
||
| return ( | ||
| <OrgUnitField | ||
| onSelectClick={this.onSelectOrgUnit} | ||
| previousOrgUnitId={this.state.previousOrgUnitId} | ||
| {...passOnProps} | ||
| /> | ||
| <React.Fragment> | ||
| <div | ||
| ref={this.anchorRef} | ||
| className={triggerClassName} | ||
| > | ||
| <input | ||
| ref={this.searchInputRef} | ||
| className={classes.searchInput} | ||
| value={open ? inputValue : ''} | ||
| onChange={this.handleInputChange} | ||
| onClick={this.openMenu} | ||
| onKeyDown={this.handleKeyDown} | ||
| readOnly={!open} | ||
| disabled={disabled} | ||
| placeholder={open ? i18n.t('Search for an organisation unit') : undefined} | ||
| aria-haspopup="tree" | ||
| aria-controls={open ? this.popoverId : undefined} | ||
| data-test="org-unit-selector-trigger" | ||
| /> | ||
| <span className={classes.chevron}> | ||
| <IconChevronDown16 /> | ||
| </span> | ||
| </div> | ||
|
Comment on lines
+228
to
+249
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 Chevron icon area does not trigger popover open The Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| {open && !disabled && this.renderPopover()} | ||
| </React.Fragment> | ||
| ); | ||
| } | ||
|
|
||
| render() { | ||
| const { value } = this.props; | ||
| return value ? this.renderSelectedOrgUnit(value) : this.renderOrgUnitField(); | ||
| return value ? this.renderSelectedOrgUnit(value) : this.renderCollapsedOrgUnitField(); | ||
| } | ||
| } | ||
| export const SingleOrgUnitSelectField = withStyles(getStyles)(SingleOrgUnitSelectFieldPlain); | ||
Uh oh!
There was an error while loading. Please reload this page.