@basic-ui/core 0.0.31 → 0.0.34

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.
Files changed (128) hide show
  1. package/build/cjs/index.js +113 -71
  2. package/build/cjs/index.js.map +1 -1
  3. package/build/esm/FocusLock/useFocusLock.js +21 -7
  4. package/build/esm/FocusLock/useFocusLock.js.map +1 -1
  5. package/build/esm/Tooltip/stateMachine.d.ts +17 -19
  6. package/build/esm/Tooltip/stateMachine.js +45 -49
  7. package/build/esm/Tooltip/stateMachine.js.map +1 -1
  8. package/build/esm/Tooltip/useTooltip.js +9 -9
  9. package/build/esm/Tooltip/useTooltip.js.map +1 -1
  10. package/build/esm/hooks/useGestureHandlers.d.ts +2 -0
  11. package/build/esm/hooks/useGestureHandlers.js +39 -7
  12. package/build/esm/hooks/useGestureHandlers.js.map +1 -1
  13. package/build/tsconfig.tsbuildinfo +383 -88
  14. package/package.json +6 -6
  15. package/src/Accordion/Accordion.story.tsx +72 -0
  16. package/src/Accordion/Accordion.tsx +51 -0
  17. package/src/Accordion/AccordionBody.tsx +53 -0
  18. package/src/Accordion/AccordionHeader.tsx +165 -0
  19. package/src/Accordion/AccordionItem.tsx +43 -0
  20. package/src/Accordion/context.ts +35 -0
  21. package/src/Accordion/index.ts +4 -0
  22. package/src/Accordion/scopeQuery.ts +7 -0
  23. package/src/Accordion/styles.css +21 -0
  24. package/src/CheckBox/CheckBox.tsx +41 -0
  25. package/src/CheckBox/index.ts +1 -0
  26. package/src/ComboBox/ComboBox.story.tsx +118 -0
  27. package/src/ComboBox/Combobox.tsx +153 -0
  28. package/src/ComboBox/ComboboxButton.tsx +60 -0
  29. package/src/ComboBox/ComboboxInput.tsx +178 -0
  30. package/src/ComboBox/ComboboxLabel.tsx +32 -0
  31. package/src/ComboBox/ComboboxList.tsx +47 -0
  32. package/src/ComboBox/ComboboxOption.tsx +107 -0
  33. package/src/ComboBox/ComboboxPopover.tsx +58 -0
  34. package/src/ComboBox/cities.ts +23194 -0
  35. package/src/ComboBox/context.ts +33 -0
  36. package/src/ComboBox/hooks.tsx +428 -0
  37. package/src/ComboBox/index.ts +8 -0
  38. package/src/ComboBox/makeHash.ts +19 -0
  39. package/src/ComboBox/scopeQuery.ts +6 -0
  40. package/src/ComboBox/styles.css +30 -0
  41. package/src/FocusLock/FocusLock.tsx +59 -0
  42. package/src/FocusLock/index.ts +1 -0
  43. package/src/FocusLock/tabUtils.ts +28 -0
  44. package/src/FocusLock/useFocusLock.ts +61 -0
  45. package/src/List/List.story.tsx +17 -0
  46. package/src/List/List.tsx +17 -0
  47. package/src/List/ListItem.tsx +23 -0
  48. package/src/List/context.ts +19 -0
  49. package/src/List/index.ts +2 -0
  50. package/src/Menu/.gitkeep +0 -0
  51. package/src/Menu/Menu.story.tsx +158 -0
  52. package/src/Menu/Menu.tsx +60 -0
  53. package/src/Menu/MenuButton.tsx +83 -0
  54. package/src/Menu/MenuItem.tsx +83 -0
  55. package/src/Menu/MenuList.tsx +201 -0
  56. package/src/Menu/MenuPopover.tsx +25 -0
  57. package/src/Menu/context.ts +32 -0
  58. package/src/Menu/index.ts +5 -0
  59. package/src/Menu/scope.ts +7 -0
  60. package/src/Menu/styles.css +42 -0
  61. package/src/Modal/Modal.story.tsx +242 -0
  62. package/src/Modal/Modal.tsx +42 -0
  63. package/src/Modal/ModalBackdrop.tsx +72 -0
  64. package/src/Modal/NavDrawer.story.tsx +157 -0
  65. package/src/Modal/index.ts +2 -0
  66. package/src/Modal/styles.css +46 -0
  67. package/src/Popover/.gitkeep +0 -0
  68. package/src/Popper/Popper.story.tsx +267 -0
  69. package/src/Popper/Popper.tsx +149 -0
  70. package/src/Popper/PopperArrow.tsx +36 -0
  71. package/src/Popper/context.ts +9 -0
  72. package/src/Popper/index.ts +3 -0
  73. package/src/Popper/styles.css +60 -0
  74. package/src/Portal/Portal.tsx +20 -0
  75. package/src/Portal/index.ts +1 -0
  76. package/src/RadioButton/RadioButton.story.tsx +73 -0
  77. package/src/RadioButton/RadioButton.tsx +48 -0
  78. package/src/RadioButton/RadioGroup.tsx +56 -0
  79. package/src/RadioButton/context.ts +19 -0
  80. package/src/RadioButton/index.ts +2 -0
  81. package/src/SkipNav/SkipNav.tsx +16 -0
  82. package/src/SkipNav/index.tsx +1 -0
  83. package/src/Spinner/Spinner.story.tsx +30 -0
  84. package/src/Spinner/Spinner.tsx +112 -0
  85. package/src/Spinner/SpinnerButton.tsx +48 -0
  86. package/src/Spinner/context.ts +21 -0
  87. package/src/Spinner/index.ts +2 -0
  88. package/src/Spinner/styles.css +23 -0
  89. package/src/Tabs/Tab.story.tsx +78 -0
  90. package/src/Tabs/Tab.tsx +131 -0
  91. package/src/Tabs/TabList.tsx +63 -0
  92. package/src/Tabs/TabPanel.tsx +52 -0
  93. package/src/Tabs/TabPanels.tsx +30 -0
  94. package/src/Tabs/Tabs.tsx +47 -0
  95. package/src/Tabs/context.ts +30 -0
  96. package/src/Tabs/index.tsx +5 -0
  97. package/src/Tabs/scopeQuery.ts +6 -0
  98. package/src/Tabs/styles.css +0 -0
  99. package/src/Tooltip/.gitkeep +0 -0
  100. package/src/Tooltip/Tooltip.story.tsx +59 -0
  101. package/src/Tooltip/Tooltip.tsx +48 -0
  102. package/src/Tooltip/index.ts +1 -0
  103. package/src/Tooltip/stateMachine.ts +196 -0
  104. package/src/Tooltip/styles.css +17 -0
  105. package/src/Tooltip/useTooltip.ts +128 -0
  106. package/src/hooks/index.ts +14 -0
  107. package/src/hooks/useAutoFocus.ts +13 -0
  108. package/src/hooks/useChildrenCounter.ts +50 -0
  109. package/src/hooks/useControlledState.ts +37 -0
  110. package/src/hooks/useFocusReturn.ts +23 -0
  111. package/src/hooks/useFocusState.ts +28 -0
  112. package/src/hooks/useGestureHandlers.ts +253 -0
  113. package/src/hooks/useId.ts +18 -0
  114. package/src/hooks/useMeasure.ts +33 -0
  115. package/src/hooks/useOnClickOutside.ts +32 -0
  116. package/src/hooks/useOnKeyDown.ts +18 -0
  117. package/src/hooks/useReducerMachine.ts +59 -0
  118. package/src/hooks/useRemoveBodyScroll.ts +37 -0
  119. package/src/hooks/useScope.ts +51 -0
  120. package/src/hooks/useThrottle.ts +19 -0
  121. package/src/index.ts +19 -0
  122. package/src/utils/assignRef.ts +27 -0
  123. package/src/utils/clamp.ts +3 -0
  124. package/src/utils/createSubscription.ts +16 -0
  125. package/src/utils/getCircularIndex.ts +7 -0
  126. package/src/utils/index.ts +4 -0
  127. package/src/utils/rubberBandClamp.ts +25 -0
  128. package/src/utils/wrapEvent.ts +20 -0
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
2
  "name": "@basic-ui/core",
3
- "version": "0.0.31",
3
+ "version": "0.0.34",
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": {
17
- "build": "concurrently \"yarn:build:*\"",
17
+ "build": "run -T concurrently \"yarn:build:*\"",
18
18
  "build:dts": "tsc -p ./tsconfig.json --isolatedModules false --declaration --emitDeclarationOnly",
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": "17f4567e8c4dc114ba8c938500976dce32393032"
39
+ "gitHead": "010fcc15ed96213f5532c44442039805530ec0c4"
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,4 @@
1
+ export * from './Accordion';
2
+ export * from './AccordionItem';
3
+ export * from './AccordionHeader';
4
+ export * from './AccordionBody';
@@ -0,0 +1,7 @@
1
+ export function headerScopeQuery(type: string, props: Record<string, unknown>) {
2
+ return props['data-accordion-header'] === '';
3
+ }
4
+
5
+ export function bodyScopeQuery(type: string, props: Record<string, unknown>) {
6
+ return props['data-accordion-body'] === '';
7
+ }
@@ -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
+ ));