Skip to content

Commit

Permalink
feat(select): imporve the focus events to export simulated ref (#579)
Browse files Browse the repository at this point in the history
* feat(select): imporve the focus events to export simulated ref

* test: improve testcase and fix warnings

* docs(select): add label and divider to props docs
  • Loading branch information
unix authored Jun 29, 2021
1 parent 2c78405 commit 98ef5d5
Show file tree
Hide file tree
Showing 13 changed files with 3,106 additions and 1,431 deletions.
3,473 changes: 2,442 additions & 1,031 deletions components/select/__tests__/__snapshots__/index.test.tsx.snap

Large diffs are not rendered by default.

177 changes: 96 additions & 81 deletions components/select/__tests__/__snapshots__/multiple.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Select Multiple should render correctly 1`] = `
"<div class=\\"select multiple \\"><span class=\\"value placeholder\\"><span><style>
"<div class=\\"select multiple \\"><input type=\\"search\\" role=\\"combobox\\" aria-haspopup=\\"listbox\\" readonly=\\"\\" unselectable=\\"on\\" aria-expanded=\\"false\\"><style>
input {
position: fixed;
top: -10000px;
left: -10000px;
opacity: 0;
z-index: -1;
width: 0;
height: 0;
padding: 0;
font-size: 0;
border: none;
}
</style><span class=\\"value placeholder\\"><span><style>
span {
overflow: hidden;
text-overflow: ellipsis;
Expand Down Expand Up @@ -91,84 +104,86 @@ exports[`Select Multiple should render correctly 1`] = `
height: 1.214em;
}
</style></svg></div><style>
.select {
display: inline-flex;
align-items: center;
user-select: none;
white-space: nowrap;
position: relative;
cursor: pointer;
max-width: 90vw;
overflow: hidden;
transition: border 150ms ease-in 0s, color 200ms ease-out 0s,
box-shadow 200ms ease 0s;
border: 1px solid #eaeaea;
border-radius: 5px;
background-color: #fff;
--select-font-size: calc(0.875 * 16px);
--select-height: calc(2.25 * 16px);
min-width: 11.5em;
width: initial;
height: var(--select-height);
padding: 0 calc(0.334 * 16px) 0
calc(0.667 * 16px);
margin: 0 0 0 0;
}
.multiple {
height: auto;
min-height: var(--select-height);
padding: calc(0.334 * 16px) calc(0.334 * 16px) calc(0.334 * 16px)
calc(0.667 * 16px);
}
.select:hover {
border-color: #000;
}
.select:hover .icon {
color: #000;
}
.value {
display: inline-flex;
flex: 1;
height: 100%;
align-items: center;
line-height: 1;
padding: 0;
margin-right: 1.25em;
font-size: var(--select-font-size);
color: #000;
width: calc(100% - 1.25em);
}
.value > :global(div),
.value > :global(div:hover) {
border-radius: 0;
background-color: transparent;
padding: 0;
margin: 0;
color: inherit;
}
.placeholder {
color: #999;
}
.icon {
position: absolute;
right: 4pt;
font-size: var(--select-font-size);
top: 50%;
bottom: 0;
transform: translateY(-50%) rotate(0deg);
pointer-events: none;
transition: transform 200ms ease;
display: flex;
align-items: center;
color: #666;
}
</style></div>"
.select {
display: inline-flex;
align-items: center;
user-select: none;
white-space: nowrap;
position: relative;
cursor: pointer;
max-width: 90vw;
overflow: hidden;
transition: border 150ms ease-in 0s, color 200ms ease-out 0s,
box-shadow 200ms ease 0s;
border: 1px solid #eaeaea;
border-radius: 5px;
background-color: #fff;
--select-font-size: calc(0.875 * 16px);
--select-height: calc(2.25 * 16px);
min-width: 11.5em;
width: initial;
height: var(--select-height);
padding: 0 calc(0.334 * 16px) 0
calc(0.667 * 16px);
margin: 0 0 0 0;
}
.multiple {
height: auto;
min-height: var(--select-height);
padding: calc(0.334 * 16px) calc(0.334 * 16px) calc(0.334 * 16px)
calc(0.667 * 16px);
}
.select.active,
.select:hover {
border-color: #000;
}
.select.active.icon,
.select:hover .icon {
color: #000;
}
.value {
display: inline-flex;
flex: 1;
height: 100%;
align-items: center;
line-height: 1;
padding: 0;
margin-right: 1.25em;
font-size: var(--select-font-size);
color: #000;
width: calc(100% - 1.25em);
}
.value > :global(div),
.value > :global(div:hover) {
border-radius: 0;
background-color: transparent;
padding: 0;
margin: 0;
color: inherit;
}
.placeholder {
color: #999;
}
.icon {
position: absolute;
right: 4pt;
font-size: var(--select-font-size);
top: 50%;
bottom: 0;
transform: translateY(-50%) rotate(0deg);
pointer-events: none;
transition: transform 200ms ease;
display: flex;
align-items: center;
color: #666;
}
</style></div>"
`;
66 changes: 66 additions & 0 deletions components/select/__tests__/events.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react'
import { mount } from 'enzyme'
import { Select } from 'components'
import { nativeEvent, updateWrapper } from 'tests/utils'

describe('Select Events', () => {
let container: HTMLDivElement

beforeEach(() => {
container = document.createElement('div')
document.body.append(container)
})

it('ref should be able to control the focus correctly', () => {
const ref = React.createRef<HTMLDivElement>()
const wrapper = mount(<Select ref={ref} />, { attachTo: container })
const input = wrapper.find('input').at(0).getDOMNode()
expect(document.activeElement?.outerHTML).not.toEqual(input.outerHTML)
ref.current?.focus()
expect(document.activeElement?.outerHTML).toEqual(input.outerHTML)
ref.current?.blur()
expect(document.activeElement?.outerHTML).not.toEqual(input.outerHTML)
})

it('should prevent mouse event when click background', async () => {
let visible = false
const handler = jest.fn().mockImplementation(next => {
visible = next
})
const wrapper = mount(<Select onDropdownVisibleChange={handler} />, {
attachTo: container,
})
expect(visible).toBe(false)
expect(handler).not.toBeCalled()
wrapper.find('.select').simulate('click', nativeEvent)
await updateWrapper(wrapper, 300)
expect(visible).toBe(true)
expect(handler).toBeCalledTimes(1)

wrapper.find('.dropdown').simulate('click', nativeEvent)
await updateWrapper(wrapper, 300)
expect(visible).toBe(true)
expect(handler).toBeCalledTimes(1)
})

it('scrollTo should be work correctly', async () => {
const ref = React.createRef<HTMLDivElement>()
const handler = jest.fn()
window.HTMLElement.prototype.scrollTo = jest.fn().mockImplementation(handler)
const wrapper = mount(
<Select ref={ref}>
<Select.Option value="hello">world</Select.Option>
</Select>,
{ attachTo: container },
)
wrapper.find('.select').simulate('click', nativeEvent)
await updateWrapper(wrapper, 300)
ref.current?.scrollTo({ top: 200 })
expect(handler).toBeCalled()
expect(() => wrapper.unmount()).not.toThrow()
})

afterEach(() => {
document.body.removeChild(container!)
})
})
22 changes: 22 additions & 0 deletions components/select/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ describe('Select', () => {
<Select>
<Select.Option label>1</Select.Option>
<Select.Option divider>1</Select.Option>
<Select.Option divider label>
1
</Select.Option>
<Select.Option value="1">1</Select.Option>
<Select.Option value="2">Option 2</Select.Option>
</Select>,
Expand Down Expand Up @@ -108,6 +111,25 @@ describe('Select', () => {
changeHandler.mockRestore()
})

it('should ignore option when prevent all', async () => {
let value = ''
const changeHandler = jest.fn().mockImplementation(val => (value = val))
const wrapper = mount(
<Select onChange={changeHandler}>
<Select.Option value="1">1</Select.Option>
<Select.Option value="2" preventAllEvents>
Option 2
</Select.Option>
</Select>,
)
wrapper.find('.select').simulate('click', nativeEvent)
wrapper.find('.select-dropdown').find('.option').at(1).simulate('click', nativeEvent)
await updateWrapper(wrapper, 350)
expect(changeHandler).not.toHaveBeenCalled()
expect(value).not.toEqual('2')
changeHandler.mockRestore()
})

it('should be change when value changed', async () => {
const wrapper = mount(
<Select>
Expand Down
2 changes: 1 addition & 1 deletion components/select/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ export type SelectComponentType = typeof Select & {
}
;(Select as SelectComponentType).Option = SelectOption

export type { SelectProps, SelectTypes } from './select'
export type { SelectProps, SelectTypes, SelectRef } from './select'
export type { SelectOptionProps } from './select-option'
export default Select as SelectComponentType
85 changes: 51 additions & 34 deletions components/select/select-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { CSSProperties } from 'react'
import React, { CSSProperties, useImperativeHandle, useRef } from 'react'
import useTheme from '../use-theme'
import { useSelectContext } from './select-context'
import Dropdown from '../shared/dropdown'
Expand All @@ -19,40 +19,57 @@ const defaultProps = {
type NativeAttrs = Omit<React.HTMLAttributes<any>, keyof Props>
export type SelectDropdownProps = Props & NativeAttrs

const SelectDropdown: React.FC<React.PropsWithChildren<SelectDropdownProps>> = ({
visible,
children,
className,
dropdownStyle,
disableMatchWidth,
getPopupContainer,
}: React.PropsWithChildren<SelectDropdownProps> & typeof defaultProps) => {
const theme = useTheme()
const { ref } = useSelectContext()
const SelectDropdown = React.forwardRef<
HTMLDivElement | null,
React.PropsWithChildren<SelectDropdownProps>
>(
(
{
visible,
children,
className,
dropdownStyle,
disableMatchWidth,
getPopupContainer,
}: React.PropsWithChildren<SelectDropdownProps> & typeof defaultProps,
dropdownRef,
) => {
const theme = useTheme()
const internalDropdownRef = useRef<HTMLDivElement | null>(null)
const { ref } = useSelectContext()
useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(
dropdownRef,
() => internalDropdownRef.current,
)

return (
<Dropdown
parent={ref}
visible={visible}
disableMatchWidth={disableMatchWidth}
getPopupContainer={getPopupContainer}>
<div className={`select-dropdown ${className}`} style={dropdownStyle}>
{children}
<style jsx>{`
.select-dropdown {
border-radius: ${theme.layout.radius};
box-shadow: ${theme.expressiveness.shadowLarge};
background-color: ${theme.palette.background};
max-height: 17em;
overflow-y: auto;
overflow-anchor: none;
padding: 0.38em 0;
}
`}</style>
</div>
</Dropdown>
)
}
return (
<Dropdown
parent={ref}
visible={visible}
disableMatchWidth={disableMatchWidth}
getPopupContainer={getPopupContainer}>
<div
ref={internalDropdownRef}
className={`select-dropdown ${className}`}
style={dropdownStyle}>
{children}
<style jsx>{`
.select-dropdown {
border-radius: ${theme.layout.radius};
box-shadow: ${theme.expressiveness.shadowLarge};
background-color: ${theme.palette.background};
max-height: 17em;
overflow-y: auto;
overflow-anchor: none;
padding: 0.38em 0;
scroll-behavior: smooth;
}
`}</style>
</div>
</Dropdown>
)
},
)

SelectDropdown.defaultProps = defaultProps
SelectDropdown.displayName = 'GeistSelectDropdown'
Expand Down
Loading

0 comments on commit 98ef5d5

Please sign in to comment.