@basic-ui/core 0.0.29 → 0.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cjs/index.js +90 -71
- package/build/cjs/index.js.map +1 -1
- package/build/esm/FocusLock/useFocusLock.js +21 -7
- package/build/esm/FocusLock/useFocusLock.js.map +1 -1
- package/build/esm/Menu/MenuList.js +7 -5
- package/build/esm/Menu/MenuList.js.map +1 -1
- package/build/esm/Tooltip/Tooltip.d.ts +1 -0
- package/build/esm/Tooltip/Tooltip.js +10 -3
- package/build/esm/Tooltip/Tooltip.js.map +1 -1
- package/build/esm/Tooltip/stateMachine.d.ts +17 -19
- package/build/esm/Tooltip/stateMachine.js +45 -49
- package/build/esm/Tooltip/stateMachine.js.map +1 -1
- package/build/esm/Tooltip/useTooltip.js +9 -9
- package/build/esm/Tooltip/useTooltip.js.map +1 -1
- package/build/tsconfig.tsbuildinfo +384 -89
- package/package.json +5 -5
- package/src/Accordion/Accordion.story.tsx +72 -0
- package/src/Accordion/Accordion.tsx +51 -0
- package/src/Accordion/AccordionBody.tsx +53 -0
- package/src/Accordion/AccordionHeader.tsx +165 -0
- package/src/Accordion/AccordionItem.tsx +43 -0
- package/src/Accordion/context.ts +35 -0
- package/src/Accordion/index.ts +4 -0
- package/src/Accordion/scopeQuery.ts +7 -0
- package/src/Accordion/styles.css +21 -0
- package/src/CheckBox/CheckBox.tsx +41 -0
- package/src/CheckBox/index.ts +1 -0
- package/src/ComboBox/ComboBox.story.tsx +118 -0
- package/src/ComboBox/Combobox.tsx +153 -0
- package/src/ComboBox/ComboboxButton.tsx +60 -0
- package/src/ComboBox/ComboboxInput.tsx +178 -0
- package/src/ComboBox/ComboboxLabel.tsx +32 -0
- package/src/ComboBox/ComboboxList.tsx +47 -0
- package/src/ComboBox/ComboboxOption.tsx +107 -0
- package/src/ComboBox/ComboboxPopover.tsx +58 -0
- package/src/ComboBox/cities.ts +23194 -0
- package/src/ComboBox/context.ts +33 -0
- package/src/ComboBox/hooks.tsx +428 -0
- package/src/ComboBox/index.ts +8 -0
- package/src/ComboBox/makeHash.ts +19 -0
- package/src/ComboBox/scopeQuery.ts +6 -0
- package/src/ComboBox/styles.css +30 -0
- package/src/FocusLock/FocusLock.tsx +59 -0
- package/src/FocusLock/index.ts +1 -0
- package/src/FocusLock/tabUtils.ts +28 -0
- package/src/FocusLock/useFocusLock.ts +61 -0
- package/src/List/List.story.tsx +17 -0
- package/src/List/List.tsx +17 -0
- package/src/List/ListItem.tsx +23 -0
- package/src/List/context.ts +19 -0
- package/src/List/index.ts +2 -0
- package/src/Menu/.gitkeep +0 -0
- package/src/Menu/Menu.story.tsx +158 -0
- package/src/Menu/Menu.tsx +60 -0
- package/src/Menu/MenuButton.tsx +83 -0
- package/src/Menu/MenuItem.tsx +83 -0
- package/src/Menu/MenuList.tsx +201 -0
- package/src/Menu/MenuPopover.tsx +25 -0
- package/src/Menu/context.ts +32 -0
- package/src/Menu/index.ts +5 -0
- package/src/Menu/scope.ts +7 -0
- package/src/Menu/styles.css +42 -0
- package/src/Modal/Modal.story.tsx +242 -0
- package/src/Modal/Modal.tsx +42 -0
- package/src/Modal/ModalBackdrop.tsx +72 -0
- package/src/Modal/NavDrawer.story.tsx +157 -0
- package/src/Modal/index.ts +2 -0
- package/src/Modal/styles.css +46 -0
- package/src/Popover/.gitkeep +0 -0
- package/src/Popper/Popper.story.tsx +267 -0
- package/src/Popper/Popper.tsx +149 -0
- package/src/Popper/PopperArrow.tsx +36 -0
- package/src/Popper/context.ts +9 -0
- package/src/Popper/index.ts +3 -0
- package/src/Popper/styles.css +60 -0
- package/src/Portal/Portal.tsx +20 -0
- package/src/Portal/index.ts +1 -0
- package/src/RadioButton/RadioButton.story.tsx +73 -0
- package/src/RadioButton/RadioButton.tsx +48 -0
- package/src/RadioButton/RadioGroup.tsx +56 -0
- package/src/RadioButton/context.ts +19 -0
- package/src/RadioButton/index.ts +2 -0
- package/src/SkipNav/SkipNav.tsx +16 -0
- package/src/SkipNav/index.tsx +1 -0
- package/src/Spinner/Spinner.story.tsx +30 -0
- package/src/Spinner/Spinner.tsx +112 -0
- package/src/Spinner/SpinnerButton.tsx +48 -0
- package/src/Spinner/context.ts +21 -0
- package/src/Spinner/index.ts +2 -0
- package/src/Spinner/styles.css +23 -0
- package/src/Tabs/Tab.story.tsx +78 -0
- package/src/Tabs/Tab.tsx +131 -0
- package/src/Tabs/TabList.tsx +63 -0
- package/src/Tabs/TabPanel.tsx +52 -0
- package/src/Tabs/TabPanels.tsx +30 -0
- package/src/Tabs/Tabs.tsx +47 -0
- package/src/Tabs/context.ts +30 -0
- package/src/Tabs/index.tsx +5 -0
- package/src/Tabs/scopeQuery.ts +6 -0
- package/src/Tabs/styles.css +0 -0
- package/src/Tooltip/.gitkeep +0 -0
- package/src/Tooltip/Tooltip.story.tsx +59 -0
- package/src/Tooltip/Tooltip.tsx +48 -0
- package/src/Tooltip/index.ts +1 -0
- package/src/Tooltip/stateMachine.ts +196 -0
- package/src/Tooltip/styles.css +17 -0
- package/src/Tooltip/useTooltip.ts +128 -0
- package/src/hooks/index.ts +14 -0
- package/src/hooks/useAutoFocus.ts +13 -0
- package/src/hooks/useChildrenCounter.ts +50 -0
- package/src/hooks/useControlledState.ts +37 -0
- package/src/hooks/useFocusReturn.ts +23 -0
- package/src/hooks/useFocusState.ts +28 -0
- package/src/hooks/useGestureHandlers.ts +217 -0
- package/src/hooks/useId.ts +18 -0
- package/src/hooks/useMeasure.ts +33 -0
- package/src/hooks/useOnClickOutside.ts +32 -0
- package/src/hooks/useOnKeyDown.ts +18 -0
- package/src/hooks/useReducerMachine.ts +59 -0
- package/src/hooks/useRemoveBodyScroll.ts +37 -0
- package/src/hooks/useScope.ts +51 -0
- package/src/hooks/useThrottle.ts +19 -0
- package/src/index.ts +19 -0
- package/src/utils/assignRef.ts +27 -0
- package/src/utils/clamp.ts +3 -0
- package/src/utils/createSubscription.ts +16 -0
- package/src/utils/getCircularIndex.ts +7 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/rubberBandClamp.ts +25 -0
- package/src/utils/wrapEvent.ts +20 -0
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basic-ui/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.33",
|
|
4
4
|
"description": "Accessible React Components used as building blocks for UI patterns",
|
|
5
5
|
"author": "Lucas Terra <lucasterra7@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"private": false,
|
|
8
7
|
"main": "./build/cjs/index.js",
|
|
9
8
|
"module": "./build/esm/index.js",
|
|
10
9
|
"jsnext:main": "./build/esm/index.js",
|
|
11
10
|
"types": "./build/esm/index.d.ts",
|
|
12
11
|
"files": [
|
|
13
|
-
"/build"
|
|
12
|
+
"/build",
|
|
13
|
+
"/src"
|
|
14
14
|
],
|
|
15
15
|
"sideEffects": false,
|
|
16
16
|
"scripts": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"build:cjs": "rollup -c ../../rollup.config.js",
|
|
20
20
|
"build:esm": "cross-env NODE_ENV=production BABEL_ENV=esm babel --config-file ../../babel.config.js ./src --extensions \".ts,.tsx,.js,.jsx\" --source-maps --out-dir ./build/esm --ignore \"**/*.story.tsx,**/*.story.ts,**/*.test.tsx,**/*.test.ts\"",
|
|
21
21
|
"build-storybook": "build-storybook -c ../../scripts/storybook -o .out",
|
|
22
|
-
"storybook": "start-storybook -p 9001 -c ../../scripts/storybook",
|
|
22
|
+
"storybook": "yarn run -T start-storybook -p 9001 -c ../../scripts/storybook",
|
|
23
23
|
"start": "yarn run storybook",
|
|
24
24
|
"serve": "http-server .out",
|
|
25
25
|
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}' --fix",
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
"react": ">=16.14.0 || >=17.0.0",
|
|
37
37
|
"react-dom": ">=16.14.0 || >=17.0.0"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "5aa740f6686e8ed8b633d4e0d173a6dd91d28bef"
|
|
40
40
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Accordion, AccordionBody, AccordionHeader, AccordionItem } from './';
|
|
3
|
+
import { storiesOf } from '@storybook/react';
|
|
4
|
+
import './styles.css';
|
|
5
|
+
|
|
6
|
+
const stories = storiesOf('Components/Accordion', module);
|
|
7
|
+
|
|
8
|
+
function AccordionControlled() {
|
|
9
|
+
const [expandedId, setExpandedId] = useState(0);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Accordion
|
|
13
|
+
expandedIndex={expandedId}
|
|
14
|
+
onChange={(e, idx) => setExpandedId(idx)}
|
|
15
|
+
>
|
|
16
|
+
<AccordionItem>
|
|
17
|
+
<AccordionHeader>Personal information</AccordionHeader>
|
|
18
|
+
<AccordionBody>
|
|
19
|
+
<input type="text" placeholder="name" />
|
|
20
|
+
<input type="text" placeholder="email" />
|
|
21
|
+
<input type="text" placeholder="phone" />
|
|
22
|
+
<input type="text" placeholder="extension" />
|
|
23
|
+
<input type="text" placeholder="country" />
|
|
24
|
+
</AccordionBody>
|
|
25
|
+
</AccordionItem>
|
|
26
|
+
<AccordionItem>
|
|
27
|
+
<AccordionHeader>Billing address</AccordionHeader>
|
|
28
|
+
<AccordionBody>
|
|
29
|
+
<input type="text" placeholder="address" />
|
|
30
|
+
<input type="text" placeholder="address 2" />
|
|
31
|
+
<input type="text" placeholder="city" />
|
|
32
|
+
<input type="text" placeholder="state" />
|
|
33
|
+
<input type="text" placeholder="zip code" />
|
|
34
|
+
</AccordionBody>
|
|
35
|
+
</AccordionItem>
|
|
36
|
+
<AccordionItem>
|
|
37
|
+
<AccordionHeader>Shipping address</AccordionHeader>
|
|
38
|
+
<AccordionBody>
|
|
39
|
+
<input type="text" placeholder="address" />
|
|
40
|
+
<input type="text" placeholder="address 2" />
|
|
41
|
+
<input type="text" placeholder="city" />
|
|
42
|
+
<input type="text" placeholder="state" />
|
|
43
|
+
<input type="text" placeholder="zip code" />
|
|
44
|
+
</AccordionBody>
|
|
45
|
+
</AccordionItem>
|
|
46
|
+
</Accordion>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function SingleAccordionItem() {
|
|
51
|
+
const [expanded, setExpanded] = useState(false);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<AccordionItem
|
|
55
|
+
expanded={expanded}
|
|
56
|
+
onChange={(e, value) => setExpanded(value)}
|
|
57
|
+
>
|
|
58
|
+
<AccordionHeader>Personal information</AccordionHeader>
|
|
59
|
+
<AccordionBody>
|
|
60
|
+
<input type="text" placeholder="name" />
|
|
61
|
+
<input type="text" placeholder="email" />
|
|
62
|
+
<input type="text" placeholder="phone" />
|
|
63
|
+
<input type="text" placeholder="extension" />
|
|
64
|
+
<input type="text" placeholder="country" />
|
|
65
|
+
</AccordionBody>
|
|
66
|
+
</AccordionItem>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
stories.add('controlled', () => <AccordionControlled />);
|
|
71
|
+
|
|
72
|
+
stories.add('single accordion, controlled', () => <SingleAccordionItem />);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { forwardRef, useRef, useState } from 'react';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
import { AccordionContextProps, AccordionProvider } from './context';
|
|
4
|
+
import { useScope } from '../hooks';
|
|
5
|
+
import { assignMultipleRefs } from '../utils';
|
|
6
|
+
|
|
7
|
+
export interface AccordionProps
|
|
8
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
9
|
+
as?: React.ElementType<any>;
|
|
10
|
+
innerAs?: React.ElementType<any>;
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
expandedIndex?: number;
|
|
13
|
+
onChange?: (
|
|
14
|
+
e: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent<HTMLDivElement>,
|
|
15
|
+
index: number
|
|
16
|
+
) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Accordion = forwardRef<HTMLDivElement, AccordionProps>(
|
|
20
|
+
function Accordion(props, forwardedRef) {
|
|
21
|
+
const {
|
|
22
|
+
as: Comp = 'div',
|
|
23
|
+
expandedIndex = -1,
|
|
24
|
+
onChange,
|
|
25
|
+
...otherProps
|
|
26
|
+
} = props;
|
|
27
|
+
const [childrenHeaderHasFocus, setChildrenHeaderHasFocus] = useState(false);
|
|
28
|
+
const ref = useRef<HTMLDivElement>();
|
|
29
|
+
|
|
30
|
+
const scope = useScope(ref);
|
|
31
|
+
|
|
32
|
+
const contextValue: AccordionContextProps = {
|
|
33
|
+
childrenHeaderHasFocus,
|
|
34
|
+
setChildrenHeaderHasFocus,
|
|
35
|
+
scope,
|
|
36
|
+
expandedIndex,
|
|
37
|
+
onChange,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<AccordionProvider value={contextValue}>
|
|
42
|
+
<Comp
|
|
43
|
+
ref={assignMultipleRefs(forwardedRef, ref)}
|
|
44
|
+
{...otherProps}
|
|
45
|
+
data-accordion-root=""
|
|
46
|
+
data-children-has-focus={childrenHeaderHasFocus}
|
|
47
|
+
/>
|
|
48
|
+
</AccordionProvider>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { forwardRef, useState, useEffect, useRef } from 'react';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
import { useAccordionItemContext, useAccordionContext } from './context';
|
|
4
|
+
import { bodyScopeQuery as scopeQuery } from './scopeQuery';
|
|
5
|
+
import { assignMultipleRefs } from '../utils';
|
|
6
|
+
|
|
7
|
+
export interface AccordionBodyProps
|
|
8
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
9
|
+
as?: React.ElementType<any>;
|
|
10
|
+
innerAs?: React.ElementType<any>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const AccordionBody = forwardRef<HTMLDivElement, AccordionBodyProps>(
|
|
14
|
+
function AccordionBody(props, forwardedRef) {
|
|
15
|
+
const { as: Comp = 'div', ...otherProps } = props;
|
|
16
|
+
const accordionItemContext = useAccordionItemContext();
|
|
17
|
+
const accordionContext = useAccordionContext();
|
|
18
|
+
const ref = useRef<HTMLDivElement>();
|
|
19
|
+
const [index, setIndex] = useState<number>();
|
|
20
|
+
|
|
21
|
+
if (!accordionItemContext) {
|
|
22
|
+
throw new Error('Missing parent <Accordion /> component');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (accordionContext) {
|
|
27
|
+
const allHeaders = accordionContext.scope.current.queryAllNodes(
|
|
28
|
+
scopeQuery
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const index = allHeaders.findIndex((e) => e === ref.current);
|
|
32
|
+
setIndex(index);
|
|
33
|
+
}
|
|
34
|
+
}, [accordionContext]);
|
|
35
|
+
|
|
36
|
+
const expanded = Boolean(
|
|
37
|
+
accordionItemContext.expanded ||
|
|
38
|
+
(accordionContext && accordionContext.expandedIndex === index)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Comp
|
|
43
|
+
ref={assignMultipleRefs(forwardedRef, ref)}
|
|
44
|
+
{...otherProps}
|
|
45
|
+
aria-labelledby={accordionItemContext.headerId}
|
|
46
|
+
id={accordionItemContext.bodyId}
|
|
47
|
+
role="region"
|
|
48
|
+
data-accordion-body=""
|
|
49
|
+
hidden={expanded ? undefined : 'hidden'}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { forwardRef, useRef, useState, useEffect } from 'react';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
import { wrapEvent, assignMultipleRefs, getCircularIndex } from '../utils';
|
|
4
|
+
import { useAccordionContext, useAccordionItemContext } from './context';
|
|
5
|
+
import { headerScopeQuery as scopeQuery } from './scopeQuery';
|
|
6
|
+
|
|
7
|
+
export interface AccordionHeaderProps
|
|
8
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
9
|
+
as?: React.ElementType<any>;
|
|
10
|
+
innerAs?: React.ElementType<any>;
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const AccordionHeader = forwardRef<HTMLDivElement, AccordionHeaderProps>(
|
|
15
|
+
function AccordionHeader(props, forwardedRef) {
|
|
16
|
+
const {
|
|
17
|
+
as: Comp = 'div',
|
|
18
|
+
onKeyDown,
|
|
19
|
+
onClick: onClickProp,
|
|
20
|
+
onFocus,
|
|
21
|
+
onBlur,
|
|
22
|
+
...otherProps
|
|
23
|
+
} = props;
|
|
24
|
+
const accordionContext = useAccordionContext();
|
|
25
|
+
const accordionItemContext = useAccordionItemContext();
|
|
26
|
+
const ref = useRef<HTMLDivElement>();
|
|
27
|
+
const [index, setIndex] = useState<number | undefined>();
|
|
28
|
+
|
|
29
|
+
if (!accordionItemContext) {
|
|
30
|
+
throw new Error('Missing parent <Accordion /> component');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (accordionContext) {
|
|
35
|
+
const allHeaders =
|
|
36
|
+
accordionContext.scope.current.queryAllNodes(scopeQuery) || [];
|
|
37
|
+
|
|
38
|
+
const index = allHeaders.findIndex((e) => e === ref.current);
|
|
39
|
+
setIndex(index);
|
|
40
|
+
}
|
|
41
|
+
}, [accordionContext]);
|
|
42
|
+
|
|
43
|
+
const onClick = wrapEvent(
|
|
44
|
+
onClickProp,
|
|
45
|
+
(e: React.MouseEvent<HTMLDivElement>) => {
|
|
46
|
+
let index = 0;
|
|
47
|
+
if (accordionItemContext.expanded) {
|
|
48
|
+
index = -1;
|
|
49
|
+
} else if (accordionContext) {
|
|
50
|
+
const allHeaders =
|
|
51
|
+
accordionContext.scope.current.queryAllNodes(scopeQuery) || [];
|
|
52
|
+
|
|
53
|
+
index = allHeaders.findIndex((e) => e === ref.current);
|
|
54
|
+
if (index === accordionContext.expandedIndex) {
|
|
55
|
+
index = -1;
|
|
56
|
+
}
|
|
57
|
+
accordionContext.onChange && accordionContext.onChange(e, index);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
accordionItemContext.onChange &&
|
|
61
|
+
accordionItemContext.onChange(e, index >= 0);
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
66
|
+
switch (e.key) {
|
|
67
|
+
case 'Enter':
|
|
68
|
+
case ' ': {
|
|
69
|
+
onClick((e as unknown) as React.MouseEvent<HTMLDivElement>);
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case 'ArrowUp':
|
|
74
|
+
case 'ArrowDown':
|
|
75
|
+
case 'Home':
|
|
76
|
+
case 'End': {
|
|
77
|
+
if (!accordionContext) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const allHeaders = accordionContext.scope.current.queryAllNodes(
|
|
81
|
+
scopeQuery
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
e.preventDefault();
|
|
85
|
+
|
|
86
|
+
if (allHeaders.length === 0) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let nextIndex = allHeaders.findIndex((e) => e === ref.current);
|
|
91
|
+
switch (e.key) {
|
|
92
|
+
case 'ArrowUp':
|
|
93
|
+
nextIndex += -1;
|
|
94
|
+
break;
|
|
95
|
+
case 'ArrowDown':
|
|
96
|
+
nextIndex += +1;
|
|
97
|
+
break;
|
|
98
|
+
case 'Home':
|
|
99
|
+
nextIndex = 0;
|
|
100
|
+
break;
|
|
101
|
+
case 'End':
|
|
102
|
+
nextIndex = -1;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// We're sure it will not be null, because we already checked for allHeaders.length > 0 above
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
108
|
+
nextIndex = getCircularIndex(nextIndex, allHeaders.length)!;
|
|
109
|
+
allHeaders[nextIndex] && allHeaders[nextIndex].focus();
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
default:
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const handleFocus = () => {
|
|
118
|
+
if (accordionContext) {
|
|
119
|
+
if (!accordionContext.childrenHeaderHasFocus) {
|
|
120
|
+
// this is needed to avoid rerendering the parent and
|
|
121
|
+
// messing up with the internal count for children/parent count
|
|
122
|
+
accordionContext.setChildrenHeaderHasFocus(true);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
|
|
128
|
+
if (accordionContext) {
|
|
129
|
+
const allHeaders = accordionContext.scope.current.queryAllNodes(
|
|
130
|
+
scopeQuery
|
|
131
|
+
);
|
|
132
|
+
const newFocusIsHeader =
|
|
133
|
+
allHeaders.findIndex((header) => header === e.relatedTarget) >= 0;
|
|
134
|
+
|
|
135
|
+
// only remove focus flag if the focus went to some element
|
|
136
|
+
// that is not an accordion header
|
|
137
|
+
if (!newFocusIsHeader) {
|
|
138
|
+
accordionContext.setChildrenHeaderHasFocus(false);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const expanded = Boolean(
|
|
144
|
+
accordionItemContext.expanded ||
|
|
145
|
+
(accordionContext && accordionContext.expandedIndex === index)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<Comp
|
|
150
|
+
ref={assignMultipleRefs(ref, forwardedRef)}
|
|
151
|
+
{...otherProps}
|
|
152
|
+
id={accordionItemContext.headerId}
|
|
153
|
+
aria-controls={accordionItemContext.bodyId}
|
|
154
|
+
role="button"
|
|
155
|
+
data-accordion-header=""
|
|
156
|
+
tabIndex="0"
|
|
157
|
+
onKeyDown={wrapEvent(onKeyDown, handleKeyDown)}
|
|
158
|
+
onFocus={wrapEvent(onFocus, handleFocus)}
|
|
159
|
+
onBlur={wrapEvent(onBlur, handleBlur)}
|
|
160
|
+
onClick={onClick}
|
|
161
|
+
aria-expanded={String(expanded)}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Fragment, forwardRef } from 'react';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
import { AccordionItemContextProps, AccordionItemProvider } from './context';
|
|
4
|
+
import { useId } from '../hooks';
|
|
5
|
+
|
|
6
|
+
export interface AccordionItemProps
|
|
7
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
8
|
+
as?: React.ElementType<any>;
|
|
9
|
+
innerAs?: React.ElementType<any>;
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
expanded?: boolean;
|
|
12
|
+
onChange?: (
|
|
13
|
+
e: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent<HTMLDivElement>,
|
|
14
|
+
value: boolean
|
|
15
|
+
) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(
|
|
19
|
+
function AccordionItem(props, forwardedRef) {
|
|
20
|
+
const {
|
|
21
|
+
as: Comp = Fragment,
|
|
22
|
+
expanded = false,
|
|
23
|
+
onChange,
|
|
24
|
+
...otherProps
|
|
25
|
+
} = props;
|
|
26
|
+
const id = useId();
|
|
27
|
+
|
|
28
|
+
const headerId = id ? `accordion-header-${id}` : undefined;
|
|
29
|
+
const bodyId = id ? `accordion-body-${id}` : undefined;
|
|
30
|
+
const contextValue: AccordionItemContextProps = {
|
|
31
|
+
headerId,
|
|
32
|
+
bodyId,
|
|
33
|
+
expanded,
|
|
34
|
+
onChange,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<AccordionItemProvider value={contextValue}>
|
|
39
|
+
<Comp ref={forwardedRef} {...otherProps} />
|
|
40
|
+
</AccordionItemProvider>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useContext, createContext } from 'react';
|
|
2
|
+
import { Scope } from '../hooks/useScope';
|
|
3
|
+
|
|
4
|
+
// AccordionGroup Component
|
|
5
|
+
export interface AccordionContextProps {
|
|
6
|
+
childrenHeaderHasFocus: boolean;
|
|
7
|
+
setChildrenHeaderHasFocus: (value: boolean) => void;
|
|
8
|
+
scope: Scope<HTMLElement>;
|
|
9
|
+
expandedIndex: number;
|
|
10
|
+
onChange?: (
|
|
11
|
+
e: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent<HTMLDivElement>,
|
|
12
|
+
index: number
|
|
13
|
+
) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const accordionContext = createContext<AccordionContextProps | null>(null);
|
|
17
|
+
export const { Provider: AccordionProvider } = accordionContext;
|
|
18
|
+
export const useAccordionContext = () => useContext(accordionContext);
|
|
19
|
+
|
|
20
|
+
// Accordion Component
|
|
21
|
+
export interface AccordionItemContextProps {
|
|
22
|
+
headerId: string | undefined;
|
|
23
|
+
bodyId: string | undefined;
|
|
24
|
+
expanded: boolean;
|
|
25
|
+
onChange?: (
|
|
26
|
+
e: React.KeyboardEvent<HTMLDivElement> | React.MouseEvent<HTMLDivElement>,
|
|
27
|
+
value: boolean
|
|
28
|
+
) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const accordionItemContext = createContext<AccordionItemContextProps | null>(
|
|
32
|
+
null
|
|
33
|
+
);
|
|
34
|
+
export const { Provider: AccordionItemProvider } = accordionItemContext;
|
|
35
|
+
export const useAccordionItemContext = () => useContext(accordionItemContext);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[data-accordion-root] {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
max-width: 300px;
|
|
4
|
+
padding: 32px;
|
|
5
|
+
border: solid 1px #aaa;
|
|
6
|
+
margin: 32px;
|
|
7
|
+
}
|
|
8
|
+
[data-accordion-root][data-children-has-focus='true'] {
|
|
9
|
+
border-color: red;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
[data-accordion-body]:not([hidden]) {
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
padding: 16px 0;
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
[data-accordion-header] {
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { forwardRef } from 'react';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
import { useControlledState } from '../hooks';
|
|
4
|
+
|
|
5
|
+
export interface CheckBoxProps
|
|
6
|
+
extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
7
|
+
as?: React.ElementType<any>;
|
|
8
|
+
innerAs?: React.ElementType<any>;
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const CheckBox = forwardRef<HTMLInputElement, CheckBoxProps>(
|
|
13
|
+
function CheckBox(props, forwardedRef) {
|
|
14
|
+
const {
|
|
15
|
+
as: Comp = 'input',
|
|
16
|
+
checked: checkedProp,
|
|
17
|
+
defaultChecked = false,
|
|
18
|
+
onChange: onChangeProp,
|
|
19
|
+
...otherProps
|
|
20
|
+
} = props;
|
|
21
|
+
const [checked, onChange] = useControlledState(
|
|
22
|
+
checkedProp,
|
|
23
|
+
onChangeProp,
|
|
24
|
+
defaultChecked,
|
|
25
|
+
(setValue) => (e) => {
|
|
26
|
+
setValue(e.target.checked);
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Comp
|
|
32
|
+
ref={forwardedRef}
|
|
33
|
+
type="checkbox"
|
|
34
|
+
checked={checked}
|
|
35
|
+
aria-checked={checked}
|
|
36
|
+
onChange={onChange}
|
|
37
|
+
{...otherProps}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './CheckBox';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
import { storiesOf } from '@storybook/react';
|
|
4
|
+
import { ComboboxOption } from './ComboboxOption';
|
|
5
|
+
import { ComboboxList } from './ComboboxList';
|
|
6
|
+
import { ComboboxPopover } from './ComboboxPopover';
|
|
7
|
+
import { ComboboxInput } from './ComboboxInput';
|
|
8
|
+
import { ComboboxLabel } from './ComboboxLabel';
|
|
9
|
+
import { Combobox } from './Combobox';
|
|
10
|
+
import cities from './cities';
|
|
11
|
+
import './styles.css';
|
|
12
|
+
|
|
13
|
+
const stories = storiesOf('Components/Combobox', module);
|
|
14
|
+
|
|
15
|
+
function useCityMatch(searchTerm: string) {
|
|
16
|
+
return useMemo(() => {
|
|
17
|
+
const term = searchTerm.trim().toLowerCase();
|
|
18
|
+
return term === ''
|
|
19
|
+
? []
|
|
20
|
+
: cities.filter(
|
|
21
|
+
(city) =>
|
|
22
|
+
city.city.toLowerCase().indexOf(term) !== -1 ||
|
|
23
|
+
city.state.toLowerCase().indexOf(term) !== -1
|
|
24
|
+
);
|
|
25
|
+
}, [searchTerm]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function UncontrolledClientSideExample({ initialValue = '' }) {
|
|
29
|
+
const [term, setTerm] = useState(initialValue);
|
|
30
|
+
const [selected, setSelected] = useState(initialValue);
|
|
31
|
+
const results = useCityMatch(term);
|
|
32
|
+
|
|
33
|
+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
34
|
+
setTerm(event.target.value);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleSelect = (value: string) => {
|
|
38
|
+
setSelected(value);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div>
|
|
43
|
+
<h2>Clientside Search</h2>
|
|
44
|
+
<p>Selection: {selected}</p>
|
|
45
|
+
<p>Term: {term}</p>
|
|
46
|
+
<Combobox onSelect={handleSelect} selectOnBlur>
|
|
47
|
+
<ComboboxLabel>Enter a city name</ComboboxLabel>
|
|
48
|
+
<br />
|
|
49
|
+
<ComboboxInput onChange={handleChange} defaultValue={initialValue} />
|
|
50
|
+
{results.length > 0 && (
|
|
51
|
+
<ComboboxPopover>
|
|
52
|
+
<ComboboxList persistSelection={true}>
|
|
53
|
+
{results.slice(0, 10).map((result, index) => (
|
|
54
|
+
<ComboboxOption
|
|
55
|
+
key={`${result.city}, ${result.state}, ${index}`}
|
|
56
|
+
id={`${result.city}, ${result.state}, ${index}`}
|
|
57
|
+
text={`${result.city}, ${result.state}`}
|
|
58
|
+
value={result}
|
|
59
|
+
/>
|
|
60
|
+
))}
|
|
61
|
+
</ComboboxList>
|
|
62
|
+
</ComboboxPopover>
|
|
63
|
+
)}
|
|
64
|
+
</Combobox>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function ControlledClientSideExample({ initialValue = '' }) {
|
|
70
|
+
const [term, setTerm] = useState(initialValue);
|
|
71
|
+
const [selected, setSelected] = useState(initialValue);
|
|
72
|
+
const results = useCityMatch(term);
|
|
73
|
+
|
|
74
|
+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
75
|
+
setTerm(event.target.value);
|
|
76
|
+
setSelected('');
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleSelect = (value: string) => {
|
|
80
|
+
setSelected(value);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div>
|
|
85
|
+
<h2>Clientside Search</h2>
|
|
86
|
+
<p>Selection: {selected}</p>
|
|
87
|
+
<p>Term: {term}</p>
|
|
88
|
+
<Combobox onSelect={handleSelect} selectOnBlur>
|
|
89
|
+
<ComboboxLabel>Enter a city name</ComboboxLabel>
|
|
90
|
+
<br />
|
|
91
|
+
<ComboboxInput onChange={handleChange} value={selected || term} />
|
|
92
|
+
{results.length > 0 && (
|
|
93
|
+
<ComboboxPopover>
|
|
94
|
+
<ComboboxList persistSelection={true}>
|
|
95
|
+
{results.slice(0, 10).map((result, index) => (
|
|
96
|
+
<ComboboxOption
|
|
97
|
+
key={`${result.city}, ${result.state}, ${index}`}
|
|
98
|
+
id={`${result.city}, ${result.state}, ${index}`}
|
|
99
|
+
text={`${result.city}, ${result.state}`}
|
|
100
|
+
value={result}
|
|
101
|
+
/>
|
|
102
|
+
))}
|
|
103
|
+
</ComboboxList>
|
|
104
|
+
</ComboboxPopover>
|
|
105
|
+
)}
|
|
106
|
+
</Combobox>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
stories.add('Uncontrolled Clientside', () => <UncontrolledClientSideExample />);
|
|
112
|
+
stories.add('Uncontrolled Clientside - Initial', () => (
|
|
113
|
+
<UncontrolledClientSideExample initialValue="Aberdeen" />
|
|
114
|
+
));
|
|
115
|
+
stories.add('Controlled Clientside', () => <ControlledClientSideExample />);
|
|
116
|
+
stories.add('Controlled Clientside - Initial', () => (
|
|
117
|
+
<ControlledClientSideExample initialValue="Aberdeen" />
|
|
118
|
+
));
|