@brillout/docpress 0.16.41 → 0.16.43
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/code-blocks/components/ChoiceGroup.css +126 -40
- package/code-blocks/components/ChoiceGroup.tsx +62 -54
- package/code-blocks/components/Tabs.css +2 -9
- package/code-blocks/components/Tabs.tsx +24 -69
- package/code-blocks/hooks/useCurrentSelection.ts +19 -10
- package/code-blocks/hooks/useMDXComponents.tsx +2 -2
- package/code-blocks/remarkChoiceGroup.ts +4 -1
- package/code-blocks/utils/generateChoiceGroupCode.ts +1 -1
- package/css/index.css +1 -0
- package/css/sr-only.css +11 -0
- package/dist/code-blocks/components/Tabs.js +8 -42
- package/dist/code-blocks/hooks/useCurrentSelection.js +23 -10
- package/dist/code-blocks/remarkChoiceGroup.js +5 -2
- package/dist/code-blocks/utils/generateChoiceGroupCode.js +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
.choice-group {
|
|
1
|
+
.choice-group-container {
|
|
2
2
|
position: relative;
|
|
3
3
|
|
|
4
4
|
/* layout */
|
|
5
5
|
--top-position: 10px;
|
|
6
|
-
--border-
|
|
6
|
+
--border-radius: 5px;
|
|
7
|
+
--transition-select-list-duration: 180ms;
|
|
8
|
+
--transition-select-option-duration: 120ms;
|
|
9
|
+
--transition-select-list: top var(--transition-select-list-duration) cubic-bezier(0.2, 0.9, 0.2, 1), clip-path
|
|
10
|
+
var(--transition-select-list-duration) ease-in-out;
|
|
11
|
+
--transition-select-option: background var(--transition-select-option-duration) ease;
|
|
12
|
+
--transition-select-icon: filter 500ms ease, opacity 500ms ease;
|
|
7
13
|
|
|
8
14
|
.choice-group__selects {
|
|
9
15
|
position: absolute;
|
|
@@ -14,39 +20,50 @@
|
|
|
14
20
|
right: 42px;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
|
-
.choice-
|
|
18
|
-
|
|
23
|
+
.choice-select__list {
|
|
24
|
+
position: relative;
|
|
19
25
|
font-size: 13.3333px;
|
|
26
|
+
background: transparent;
|
|
27
|
+
height: calc(var(--choice-count) * var(--option-height));
|
|
28
|
+
top: 0;
|
|
20
29
|
|
|
21
30
|
-webkit-user-select: none; /* Safari */
|
|
22
31
|
-moz-user-select: none; /* Firefox */
|
|
23
32
|
-ms-user-select: none; /* IE/Edge legacy */
|
|
24
33
|
user-select: none; /* Standard */
|
|
25
34
|
|
|
26
|
-
border-radius:
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
border-radius: var(--border-radius);
|
|
36
|
+
width: 100%;
|
|
37
|
+
transition: var(--transition-select-list);
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
.choice-
|
|
32
|
-
position:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
.choice-select__border {
|
|
41
|
+
position: absolute;
|
|
42
|
+
left: 0;
|
|
43
|
+
right: 0;
|
|
44
|
+
height: var(--option-height);
|
|
45
|
+
|
|
46
|
+
border-style: solid;
|
|
47
|
+
border-width: 1px 2px 2px 1px;
|
|
48
|
+
border-color: hsl(0, 0%, 75%) hsl(0, 0%, 72%) hsl(0, 0%, 72%) hsl(0, 0%, 75%);
|
|
49
|
+
|
|
50
|
+
border-radius: var(--border-radius);
|
|
51
|
+
pointer-events: none;
|
|
52
|
+
|
|
53
|
+
transition: top var(--transition-select-list-duration) ease-in-out, height var(--transition-select-list-duration)
|
|
54
|
+
ease-in-out;
|
|
38
55
|
}
|
|
39
56
|
|
|
40
57
|
.choice-select__option {
|
|
41
58
|
display: flex;
|
|
59
|
+
height: var(--option-height);
|
|
42
60
|
white-space: nowrap;
|
|
43
61
|
align-items: center;
|
|
44
62
|
flex-wrap: nowrap;
|
|
45
63
|
background: #fff;
|
|
46
64
|
padding: 0 6px;
|
|
47
|
-
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
48
65
|
cursor: pointer;
|
|
49
|
-
transition:
|
|
66
|
+
transition: var(--transition-select-option);
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
.choice-select__option-content {
|
|
@@ -61,54 +78,123 @@
|
|
|
61
78
|
|
|
62
79
|
filter: grayscale(100%);
|
|
63
80
|
opacity: 0.8;
|
|
64
|
-
|
|
65
|
-
transition: filter var(--transition), opacity var(--transition);
|
|
81
|
+
transition: var(--transition-select-icon);
|
|
66
82
|
}
|
|
67
83
|
|
|
68
|
-
.choice-
|
|
69
|
-
.choice-
|
|
84
|
+
.choice-select__list:hover .choice-select__option img,
|
|
85
|
+
.choice-select__list[aria-expanded='true'] .choice-select__option img {
|
|
70
86
|
filter: grayscale(0%);
|
|
71
87
|
opacity: 1;
|
|
72
88
|
}
|
|
73
89
|
|
|
74
|
-
.choice-
|
|
75
|
-
overflow: hidden;
|
|
76
|
-
border-width: 1px 2px 2px 1px;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
.choice-select[aria-expanded='true'] {
|
|
80
|
-
overflow: visible;
|
|
81
|
-
border-width: 0;
|
|
90
|
+
.choice-select__list[aria-expanded='true'] {
|
|
82
91
|
z-index: 1;
|
|
92
|
+
clip-path: inset(0px round var(--border-radius)) !important;
|
|
93
|
+
.choice-select__border {
|
|
94
|
+
top: 0 !important;
|
|
95
|
+
height: calc(var(--choice-count) * var(--option-height));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
83
98
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
.choice-select__list:not(.hovered) {
|
|
100
|
+
transition: none;
|
|
101
|
+
.choice-select__border {
|
|
102
|
+
transition: none;
|
|
103
|
+
}
|
|
104
|
+
.choice-select__option {
|
|
105
|
+
transition: none;
|
|
87
106
|
}
|
|
88
107
|
}
|
|
89
108
|
|
|
90
|
-
.choice-select__option:last-child {
|
|
91
|
-
|
|
109
|
+
.choice-select__option:not(:last-child) {
|
|
110
|
+
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.1);
|
|
92
111
|
}
|
|
93
112
|
|
|
94
113
|
.choice-select__option:hover {
|
|
95
114
|
background: #f5f5f5;
|
|
96
115
|
}
|
|
97
116
|
|
|
98
|
-
.choice-select__option
|
|
117
|
+
.choice-select__option:has(.choice-select__radio:checked) {
|
|
99
118
|
background: #eee;
|
|
100
119
|
}
|
|
101
120
|
|
|
102
|
-
.choice-select__option[aria-disabled='true'] {
|
|
103
|
-
color: #999;
|
|
104
|
-
cursor: not-allowed;
|
|
105
|
-
opacity: 0.8;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
121
|
.hidden {
|
|
109
122
|
display: none !important;
|
|
110
123
|
}
|
|
111
124
|
|
|
125
|
+
.choice-select__list:has(.choice-select__option:nth-of-type(1) .choice-select__radio:checked) {
|
|
126
|
+
top: 0;
|
|
127
|
+
clip-path: inset(
|
|
128
|
+
calc(0 * var(--option-height)) 0 calc((var(--choice-count) - 0 - 1) * var(--option-height)) 0 round
|
|
129
|
+
var(--border-radius)
|
|
130
|
+
);
|
|
131
|
+
.choice-select__border {
|
|
132
|
+
top: 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
.choice-select__list:has(.choice-select__option:nth-of-type(2) .choice-select__radio:checked) {
|
|
136
|
+
top: calc(-1 * var(--option-height));
|
|
137
|
+
clip-path: inset(
|
|
138
|
+
calc(1 * var(--option-height)) 0 calc((var(--choice-count) - 1 - 1) * var(--option-height)) 0 round
|
|
139
|
+
var(--border-radius)
|
|
140
|
+
);
|
|
141
|
+
.choice-select__border {
|
|
142
|
+
top: var(--option-height);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
.choice-select__list:has(.choice-select__option:nth-of-type(3) .choice-select__radio:checked) {
|
|
146
|
+
top: calc(-2 * var(--option-height));
|
|
147
|
+
clip-path: inset(
|
|
148
|
+
calc(2 * var(--option-height)) 0 calc((var(--choice-count) - 2 - 1) * var(--option-height)) 0 round
|
|
149
|
+
var(--border-radius)
|
|
150
|
+
);
|
|
151
|
+
.choice-select__border {
|
|
152
|
+
top: calc(2 * var(--option-height));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
.choice-select__list:has(.choice-select__option:nth-of-type(4) .choice-select__radio:checked) {
|
|
156
|
+
top: calc(-3 * var(--option-height));
|
|
157
|
+
clip-path: inset(
|
|
158
|
+
calc(3 * var(--option-height)) 0 calc((var(--choice-count) - 3 - 1) * var(--option-height)) 0 round
|
|
159
|
+
var(--border-radius)
|
|
160
|
+
);
|
|
161
|
+
.choice-select__border {
|
|
162
|
+
top: calc(3 * var(--option-height));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
.choice-select__list:has(.choice-select__option:nth-of-type(5) .choice-select__radio:checked) {
|
|
166
|
+
top: calc(-4 * var(--option-height));
|
|
167
|
+
clip-path: inset(
|
|
168
|
+
calc(4 * var(--option-height)) 0 calc((var(--choice-count) - 4 - 1) * var(--option-height)) 0 round
|
|
169
|
+
var(--border-radius)
|
|
170
|
+
);
|
|
171
|
+
.choice-select__border {
|
|
172
|
+
top: calc(4 * var(--option-height));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
.choice-select__list:has(.choice-select__option:nth-of-type(6) .choice-select__radio:checked) {
|
|
176
|
+
top: calc(-5 * var(--option-height));
|
|
177
|
+
clip-path: inset(
|
|
178
|
+
calc(5 * var(--option-height)) 0 calc((var(--choice-count) - 5 - 1) * var(--option-height)) 0 round
|
|
179
|
+
var(--border-radius)
|
|
180
|
+
);
|
|
181
|
+
.choice-select__border {
|
|
182
|
+
top: calc(5 * var(--option-height));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
.choice-select__list:has(.choice-select__option:nth-of-type(7) .choice-select__radio:checked) {
|
|
186
|
+
top: calc(-6 * var(--option-height));
|
|
187
|
+
clip-path: inset(
|
|
188
|
+
calc(6 * var(--option-height)) 0 calc((var(--choice-count) - 6 - 1) * var(--option-height)) 0 round
|
|
189
|
+
var(--border-radius)
|
|
190
|
+
);
|
|
191
|
+
.choice-select__border {
|
|
192
|
+
top: calc(6 * var(--option-height));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.choice-group {
|
|
112
198
|
/* choice visibility logic */
|
|
113
199
|
select:has(option:nth-of-type(1):not(:checked)) ~ .choice:nth-of-type(1),
|
|
114
200
|
select:has(option:nth-of-type(2):not(:checked)) ~ .choice:nth-of-type(2),
|
|
@@ -1,37 +1,40 @@
|
|
|
1
|
-
export { ChoiceGroup,
|
|
1
|
+
export { ChoiceGroup, ChoiceGroupContainer }
|
|
2
2
|
|
|
3
3
|
import type { ChoiceGroup as TChoiceGroup, ChoiceGroupWithParent } from '../types.js'
|
|
4
|
-
import React, {
|
|
4
|
+
import React, { useId, useState } from 'react'
|
|
5
5
|
import { usePageContext } from '../../renderer/usePageContext.js'
|
|
6
6
|
import { useCurrentSelection } from '../hooks/useCurrentSelection.js'
|
|
7
7
|
import { useRestoreScroll } from '../hooks/useRestoreScroll.js'
|
|
8
8
|
import { cls } from '../../utils/cls.js'
|
|
9
9
|
import './ChoiceGroup.css'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
function useCustomSelectsContext() {
|
|
14
|
-
const ctx = useContext(CustomSelectsContainerContext)
|
|
15
|
-
if (!ctx) throw new Error('useCustomSelectsContext must be used inside provider')
|
|
16
|
-
return ctx
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function CustomSelectsContainer({
|
|
11
|
+
function ChoiceGroupContainer({
|
|
20
12
|
children,
|
|
21
13
|
choiceGroupAll,
|
|
22
14
|
}: { children: React.ReactNode; choiceGroupAll: ChoiceGroupWithParent[] }) {
|
|
23
|
-
|
|
15
|
+
const renderCustomSelect = choiceGroupAll.some((choiceGroup) => choiceGroup.lvl === 0 && !choiceGroup.hidden)
|
|
16
|
+
return (
|
|
17
|
+
<div className="choice-group-container">
|
|
18
|
+
{children}
|
|
19
|
+
{renderCustomSelect && (
|
|
20
|
+
<div className={`choice-group__selects`}>
|
|
21
|
+
{(choiceGroupAll ?? []).map((choiceGroup) => (
|
|
22
|
+
<CustomSelect key={choiceGroup.name} choiceGroup={choiceGroup} />
|
|
23
|
+
))}
|
|
24
|
+
</div>
|
|
25
|
+
)}
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
function ChoiceGroup({ children, choiceGroup }: { children: React.ReactNode; choiceGroup: TChoiceGroup }) {
|
|
27
|
-
const { name: groupName, choices, default: defaultChoice
|
|
31
|
+
const { name: groupName, choices, default: defaultChoice } = choiceGroup
|
|
28
32
|
const [selectedChoice] = useCurrentSelection(groupName, defaultChoice)
|
|
29
|
-
const { choiceGroupAll } = useCustomSelectsContext()
|
|
30
33
|
|
|
31
34
|
return (
|
|
32
|
-
<div
|
|
35
|
+
<div className="choice-group">
|
|
33
36
|
{/* Hidden select used to control choice visibility via CSS */}
|
|
34
|
-
<select name={`choicesFor-${groupName}`} value={selectedChoice} hidden disabled>
|
|
37
|
+
<select data-choice-group={groupName} name={`choicesFor-${groupName}`} value={selectedChoice} hidden disabled>
|
|
35
38
|
{choices.map(({ name: choice }) => (
|
|
36
39
|
<option key={choice} value={choice}>
|
|
37
40
|
{choice}
|
|
@@ -39,23 +42,18 @@ function ChoiceGroup({ children, choiceGroup }: { children: React.ReactNode; cho
|
|
|
39
42
|
))}
|
|
40
43
|
</select>
|
|
41
44
|
{children}
|
|
42
|
-
{lvl === 0 && !choiceGroup.hidden && (
|
|
43
|
-
<div className={`choice-group__selects`}>
|
|
44
|
-
{choiceGroupAll.map((choiceGroup) => (
|
|
45
|
-
<CustomSelect key={choiceGroup.name} choiceGroup={choiceGroup} />
|
|
46
|
-
))}
|
|
47
|
-
</div>
|
|
48
|
-
)}
|
|
49
45
|
</div>
|
|
50
46
|
)
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
const OPTION_HEIGHT = 25
|
|
54
50
|
function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
|
|
51
|
+
const radioId = useId()
|
|
55
52
|
const choicesAll = usePageContext().config.docpress.choices
|
|
56
53
|
const { name: groupName, emptyChoices, default: defaultChoice, hidden, parentChoiceGroup, isBuiltIn } = choiceGroup
|
|
57
54
|
const [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice)
|
|
58
55
|
const [expanded, setExpanded] = useState(false)
|
|
56
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
59
57
|
const [parentSelectedChoice] = useCurrentSelection(parentChoiceGroup?.name || '', parentChoiceGroup?.default || '')
|
|
60
58
|
const setPrevPosition = useRestoreScroll([selectedChoice])
|
|
61
59
|
|
|
@@ -64,44 +62,53 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
|
|
|
64
62
|
const isEmptyChoice = (choice: string) => emptyChoices.includes(choice)
|
|
65
63
|
const filteredChoices = choices.filter((choice) => !isEmptyChoice(choice.name))
|
|
66
64
|
const selectedIndex = filteredChoices.findIndex((choice) => choice.name === selectedChoice)
|
|
67
|
-
const rectTop = -selectedIndex * OPTION_HEIGHT
|
|
68
65
|
|
|
69
66
|
return (
|
|
70
67
|
<div
|
|
71
68
|
id={`choicesFor-${groupName}`}
|
|
72
|
-
aria-haspopup="listbox"
|
|
73
69
|
aria-expanded={expanded}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
role="radiogroup"
|
|
71
|
+
className={cls([
|
|
72
|
+
'choice-select__list',
|
|
73
|
+
(isHidden || isEmptyChoice(selectedChoice)) && 'hidden',
|
|
74
|
+
isHovered && 'hovered',
|
|
75
|
+
])}
|
|
76
|
+
style={{ '--option-height': `${OPTION_HEIGHT}px`, '--choice-count': filteredChoices.length }}
|
|
77
|
+
onMouseEnter={() => {
|
|
78
|
+
setExpanded(true)
|
|
79
|
+
setIsHovered(true)
|
|
80
|
+
}}
|
|
77
81
|
onMouseLeave={() => setExpanded(false)}
|
|
82
|
+
onTransitionEnd={() => {
|
|
83
|
+
if (!expanded) setIsHovered(false)
|
|
84
|
+
}}
|
|
78
85
|
onClick={() => {
|
|
79
86
|
if (!expanded) next()
|
|
80
87
|
}}
|
|
88
|
+
data-choice-group={groupName}
|
|
81
89
|
>
|
|
82
|
-
<div
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
</div>
|
|
90
|
+
<div className="choice-select__border" />
|
|
91
|
+
{filteredChoices.map(({ name: choice, icon, iconStyle }) => (
|
|
92
|
+
<label
|
|
93
|
+
id={`choice-${choice}`}
|
|
94
|
+
key={choice}
|
|
95
|
+
className={`choice-select__option`}
|
|
96
|
+
onClick={(e) => handleOnClick(e, choice)}
|
|
97
|
+
>
|
|
98
|
+
<input
|
|
99
|
+
type="radio"
|
|
100
|
+
className="choice-select__radio sr-only"
|
|
101
|
+
name={`radio-${radioId}`}
|
|
102
|
+
value={choice}
|
|
103
|
+
checked={selectedChoice === choice}
|
|
104
|
+
readOnly
|
|
105
|
+
/>
|
|
106
|
+
<span className="choice-select__option-content">
|
|
107
|
+
<img src={icon} alt="" aria-hidden="true" style={iconStyle} />
|
|
108
|
+
{choice}
|
|
109
|
+
</span>
|
|
110
|
+
</label>
|
|
111
|
+
))}
|
|
105
112
|
</div>
|
|
106
113
|
)
|
|
107
114
|
|
|
@@ -109,11 +116,12 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
|
|
|
109
116
|
const nextIndex = (selectedIndex + 1) % filteredChoices.length
|
|
110
117
|
setSelectedChoice(filteredChoices[nextIndex]!.name)
|
|
111
118
|
}
|
|
112
|
-
function handleOnClick(e: React.MouseEvent<
|
|
113
|
-
e.
|
|
119
|
+
function handleOnClick(e: React.MouseEvent<HTMLLabelElement, MouseEvent>, choice: string) {
|
|
120
|
+
e.preventDefault()
|
|
114
121
|
const el = e.currentTarget
|
|
115
122
|
setPrevPosition(el)
|
|
116
|
-
|
|
123
|
+
const isSame = selectedChoice === choice
|
|
124
|
+
if (isSame) {
|
|
117
125
|
next()
|
|
118
126
|
} else {
|
|
119
127
|
setSelectedChoice(choice)
|
|
@@ -2,16 +2,9 @@
|
|
|
2
2
|
-webkit-tap-highlight-color: transparent;
|
|
3
3
|
|
|
4
4
|
/* tablist style logic */
|
|
5
|
-
|
|
6
|
-
select:has(option:nth-of-type(2):checked) ~ ul[role='tablist'] li:nth-of-type(2),
|
|
7
|
-
select:has(option:nth-of-type(3):checked) ~ ul[role='tablist'] li:nth-of-type(3),
|
|
8
|
-
select:has(option:nth-of-type(4):checked) ~ ul[role='tablist'] li:nth-of-type(4),
|
|
9
|
-
select:has(option:nth-of-type(5):checked) ~ ul[role='tablist'] li:nth-of-type(5),
|
|
10
|
-
select:has(option:nth-of-type(6):checked) ~ ul[role='tablist'] li:nth-of-type(6),
|
|
11
|
-
select:has(option:nth-of-type(7):checked) ~ ul[role='tablist'] li:nth-of-type(7) {
|
|
5
|
+
.choice-tabs__tab:has(.choice-tabs__radio:checked) {
|
|
12
6
|
border-bottom: 2px solid #aaa;
|
|
13
7
|
color: var(--color-text);
|
|
14
|
-
font-weight: 600;
|
|
15
8
|
}
|
|
16
9
|
}
|
|
17
10
|
|
|
@@ -36,7 +29,7 @@
|
|
|
36
29
|
.choice-tabs__tab-content {
|
|
37
30
|
display: inline-flex;
|
|
38
31
|
align-items: center;
|
|
39
|
-
gap:
|
|
32
|
+
gap: 6px;
|
|
40
33
|
}
|
|
41
34
|
|
|
42
35
|
.choice-tabs__tab img {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { Tabs }
|
|
2
2
|
|
|
3
|
-
import React from 'react'
|
|
3
|
+
import React, { useId } from 'react'
|
|
4
4
|
import { useCurrentSelection } from '../hooks/useCurrentSelection.js'
|
|
5
5
|
import { useRestoreScroll } from '../hooks/useRestoreScroll.js'
|
|
6
6
|
import { usePageContext } from '../../renderer/usePageContext.js'
|
|
@@ -8,89 +8,44 @@ import { assertUsage } from '../../utils/assert.js'
|
|
|
8
8
|
import './Tabs.css'
|
|
9
9
|
|
|
10
10
|
function Tabs({ choice, hide = [] }: { choice: string; hide: string[] }) {
|
|
11
|
+
const radioId = useId()
|
|
11
12
|
const groupName = choice
|
|
12
13
|
const pageContext = usePageContext()
|
|
13
14
|
const choicesAll = pageContext.config.docpress.choices
|
|
14
15
|
assertUsage(choicesAll && choicesAll[groupName], `${groupName} is unknown`)
|
|
15
|
-
|
|
16
16
|
const { choices, default: defaultChoice } = choicesAll[groupName]
|
|
17
17
|
const [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice)
|
|
18
18
|
const setPrevPosition = useRestoreScroll([selectedChoice])
|
|
19
19
|
const isHidden = (choice: string) => hide.includes(choice)
|
|
20
|
-
const filteredChoices = choices.filter((choice) => !isHidden(choice.name))
|
|
21
|
-
const selectedIndex = filteredChoices.findIndex((choice) => choice.name === selectedChoice)
|
|
22
20
|
|
|
23
21
|
return (
|
|
24
|
-
<div className="choice-tabs"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
onKeyDown={handleOnKeyDown}
|
|
45
|
-
>
|
|
22
|
+
<div className="choice-tabs">
|
|
23
|
+
<div
|
|
24
|
+
id={`choicesFor-${groupName}`}
|
|
25
|
+
className="choice-tabs__tab-list"
|
|
26
|
+
role="radiogroup"
|
|
27
|
+
data-choice-group={groupName}
|
|
28
|
+
>
|
|
29
|
+
{choices.map(({ name: choice, icon, iconStyle }) => (
|
|
30
|
+
<label key={choice} className="choice-tabs__tab" style={{ display: isHidden(choice) ? 'none' : undefined }}>
|
|
31
|
+
<input
|
|
32
|
+
className="choice-tabs__radio sr-only"
|
|
33
|
+
type="radio"
|
|
34
|
+
name={`radio-${radioId}`}
|
|
35
|
+
value={choice}
|
|
36
|
+
checked={selectedChoice === choice}
|
|
37
|
+
onChange={(e) => {
|
|
38
|
+
setPrevPosition(e.currentTarget)
|
|
39
|
+
setSelectedChoice(choice)
|
|
40
|
+
}}
|
|
41
|
+
/>
|
|
46
42
|
<span className="choice-tabs__tab-content">
|
|
47
43
|
<img src={icon} alt="" aria-hidden="true" style={iconStyle} />
|
|
48
44
|
{choice}
|
|
49
45
|
</span>
|
|
50
|
-
</
|
|
46
|
+
</label>
|
|
51
47
|
))}
|
|
52
|
-
</
|
|
48
|
+
</div>
|
|
53
49
|
</div>
|
|
54
50
|
)
|
|
55
|
-
|
|
56
|
-
function handleOnClick(e: React.MouseEvent<HTMLLIElement, MouseEvent>, choice: string) {
|
|
57
|
-
setPrevPosition(e.currentTarget)
|
|
58
|
-
setSelectedChoice(choice)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function handleOnKeyDown(e: React.KeyboardEvent<HTMLLIElement>) {
|
|
62
|
-
const el = e.currentTarget
|
|
63
|
-
let nextIndex = selectedIndex
|
|
64
|
-
|
|
65
|
-
switch (e.key) {
|
|
66
|
-
case 'ArrowRight':
|
|
67
|
-
nextIndex = (selectedIndex + 1) % filteredChoices.length
|
|
68
|
-
break
|
|
69
|
-
case 'ArrowLeft':
|
|
70
|
-
nextIndex = (selectedIndex - 1 + filteredChoices.length) % filteredChoices.length
|
|
71
|
-
break
|
|
72
|
-
case 'Home':
|
|
73
|
-
nextIndex = 0
|
|
74
|
-
break
|
|
75
|
-
case 'End':
|
|
76
|
-
nextIndex = filteredChoices.length - 1
|
|
77
|
-
break
|
|
78
|
-
default:
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
e.preventDefault()
|
|
83
|
-
setPrevPosition(el)
|
|
84
|
-
const nextChoice = filteredChoices[nextIndex]!
|
|
85
|
-
setSelectedChoice(nextChoice.name)
|
|
86
|
-
const tabEl = el.parentElement?.parentElement as HTMLDivElement
|
|
87
|
-
|
|
88
|
-
if (!isInViewport(tabEl)) tabEl.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
|
89
|
-
el.focus()
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function isInViewport(el: Element) {
|
|
94
|
-
const rect = el.getBoundingClientRect()
|
|
95
|
-
return rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth
|
|
96
51
|
}
|
|
@@ -19,19 +19,28 @@ function useCurrentSelection(choiceGroupName: string, defaultValue: string) {
|
|
|
19
19
|
// WARNING: We cannot use `keyPrefix` here: closures don't work because we serialize the function.
|
|
20
20
|
const initializeChoiceGroup_SSR = `initializeChoiceGroup();${initializeChoiceGroup.toString()};`
|
|
21
21
|
function initializeChoiceGroup() {
|
|
22
|
-
const groupsElements =
|
|
22
|
+
const groupsElements = [
|
|
23
|
+
...document.querySelectorAll('select[data-choice-group]'),
|
|
24
|
+
...document.querySelectorAll('div[data-choice-group]'),
|
|
25
|
+
]
|
|
23
26
|
for (const groupEl of groupsElements) {
|
|
24
|
-
const choiceGroupName = groupEl.getAttribute('data-choice-group')
|
|
27
|
+
const choiceGroupName = groupEl.getAttribute('data-choice-group')
|
|
28
|
+
if (!choiceGroupName) continue
|
|
25
29
|
const storageKey = `docpress:choice:${choiceGroupName}`
|
|
26
30
|
const selectedChoice = localStorage.getItem(storageKey)
|
|
27
|
-
if (selectedChoice)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
selectEl.value = selectedChoice
|
|
34
|
-
|
|
31
|
+
if (!selectedChoice) continue
|
|
32
|
+
switch (groupEl.tagName) {
|
|
33
|
+
case 'SELECT':
|
|
34
|
+
const selectEl = groupEl as HTMLSelectElement
|
|
35
|
+
const optionExists = [...selectEl.options].some((opt) => opt.value === selectedChoice)
|
|
36
|
+
if (!optionExists) localStorage.removeItem(storageKey)
|
|
37
|
+
else selectEl.value = selectedChoice
|
|
38
|
+
break
|
|
39
|
+
case 'DIV':
|
|
40
|
+
const radioEl = groupEl.querySelector<HTMLInputElement>(`input[type="radio"][value="${selectedChoice}"]`)
|
|
41
|
+
if (radioEl) radioEl.checked = true
|
|
42
|
+
default:
|
|
43
|
+
break
|
|
35
44
|
}
|
|
36
45
|
}
|
|
37
46
|
}
|
|
@@ -3,12 +3,12 @@ export { useMDXComponents }
|
|
|
3
3
|
import React from 'react'
|
|
4
4
|
import type { UseMdxComponents } from '@mdx-js/mdx'
|
|
5
5
|
import { Pre } from '../components/Pre.js'
|
|
6
|
-
import { ChoiceGroup,
|
|
6
|
+
import { ChoiceGroup, ChoiceGroupContainer } from '../components/ChoiceGroup.js'
|
|
7
7
|
|
|
8
8
|
const useMDXComponents: UseMdxComponents = () => {
|
|
9
9
|
return {
|
|
10
10
|
ChoiceGroup,
|
|
11
|
-
|
|
11
|
+
ChoiceGroupContainer,
|
|
12
12
|
pre: (props) => <Pre {...props} />,
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -83,7 +83,10 @@ const remarkChoiceGroup: Plugin<[], Root> = (): Transformer<Root> => {
|
|
|
83
83
|
remarkPkgManager.call(this)(tree, file)
|
|
84
84
|
|
|
85
85
|
visit(tree, 'mdxJsxFlowElement', (node) => {
|
|
86
|
-
|
|
86
|
+
// Descend into non-container nodes so that a `CustomSelectsContainer` nested inside another JSX
|
|
87
|
+
// element (e.g. react-tabs `<Tabs>`/`<TabPanel>`, or a `<div>`) still gets visited and its
|
|
88
|
+
// `choiceGroupAll` attribute injected. (Returning 'skip' here would stop the descent.)
|
|
89
|
+
if (node.name !== 'ChoiceGroupContainer') return
|
|
87
90
|
|
|
88
91
|
const choiceGroupAll: ChoiceGroupWithParent[] = []
|
|
89
92
|
|
|
@@ -130,7 +130,7 @@ function generateChoiceGroupCode(choiceNodes: ChoiceNode[], parent: Parent, hide
|
|
|
130
130
|
if (lvl === 0) {
|
|
131
131
|
return {
|
|
132
132
|
type: 'mdxJsxFlowElement',
|
|
133
|
-
name: '
|
|
133
|
+
name: 'ChoiceGroupContainer',
|
|
134
134
|
attributes: [],
|
|
135
135
|
children: [choiceGroupNode],
|
|
136
136
|
}
|
package/css/index.css
CHANGED
package/css/sr-only.css
ADDED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
export { Tabs };
|
|
2
|
-
import React from 'react';
|
|
2
|
+
import React, { useId } from 'react';
|
|
3
3
|
import { useCurrentSelection } from '../hooks/useCurrentSelection.js';
|
|
4
4
|
import { useRestoreScroll } from '../hooks/useRestoreScroll.js';
|
|
5
5
|
import { usePageContext } from '../../renderer/usePageContext.js';
|
|
6
6
|
import { assertUsage } from '../../utils/assert.js';
|
|
7
7
|
import './Tabs.css';
|
|
8
8
|
function Tabs({ choice, hide = [] }) {
|
|
9
|
+
const radioId = useId();
|
|
9
10
|
const groupName = choice;
|
|
10
11
|
const pageContext = usePageContext();
|
|
11
12
|
const choicesAll = pageContext.config.docpress.choices;
|
|
@@ -14,48 +15,13 @@ function Tabs({ choice, hide = [] }) {
|
|
|
14
15
|
const [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice);
|
|
15
16
|
const setPrevPosition = useRestoreScroll([selectedChoice]);
|
|
16
17
|
const isHidden = (choice) => hide.includes(choice);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
return (React.createElement("div", { className: "choice-tabs" },
|
|
19
|
+
React.createElement("div", { id: `choicesFor-${groupName}`, className: "choice-tabs__tab-list", role: "radiogroup", "data-choice-group": groupName }, choices.map(({ name: choice, icon, iconStyle }) => (React.createElement("label", { key: choice, className: "choice-tabs__tab", style: { display: isHidden(choice) ? 'none' : undefined } },
|
|
20
|
+
React.createElement("input", { className: "choice-tabs__radio sr-only", type: "radio", name: `radio-${radioId}`, value: choice, checked: selectedChoice === choice, onChange: (e) => {
|
|
21
|
+
setPrevPosition(e.currentTarget);
|
|
22
|
+
setSelectedChoice(choice);
|
|
23
|
+
} }),
|
|
22
24
|
React.createElement("span", { className: "choice-tabs__tab-content" },
|
|
23
25
|
React.createElement("img", { src: icon, alt: "", "aria-hidden": "true", style: iconStyle }),
|
|
24
26
|
choice)))))));
|
|
25
|
-
function handleOnClick(e, choice) {
|
|
26
|
-
setPrevPosition(e.currentTarget);
|
|
27
|
-
setSelectedChoice(choice);
|
|
28
|
-
}
|
|
29
|
-
function handleOnKeyDown(e) {
|
|
30
|
-
const el = e.currentTarget;
|
|
31
|
-
let nextIndex = selectedIndex;
|
|
32
|
-
switch (e.key) {
|
|
33
|
-
case 'ArrowRight':
|
|
34
|
-
nextIndex = (selectedIndex + 1) % filteredChoices.length;
|
|
35
|
-
break;
|
|
36
|
-
case 'ArrowLeft':
|
|
37
|
-
nextIndex = (selectedIndex - 1 + filteredChoices.length) % filteredChoices.length;
|
|
38
|
-
break;
|
|
39
|
-
case 'Home':
|
|
40
|
-
nextIndex = 0;
|
|
41
|
-
break;
|
|
42
|
-
case 'End':
|
|
43
|
-
nextIndex = filteredChoices.length - 1;
|
|
44
|
-
break;
|
|
45
|
-
default:
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
e.preventDefault();
|
|
49
|
-
setPrevPosition(el);
|
|
50
|
-
const nextChoice = filteredChoices[nextIndex];
|
|
51
|
-
setSelectedChoice(nextChoice.name);
|
|
52
|
-
const tabEl = el.parentElement?.parentElement;
|
|
53
|
-
if (!isInViewport(tabEl))
|
|
54
|
-
tabEl.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
|
55
|
-
el.focus();
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
function isInViewport(el) {
|
|
59
|
-
const rect = el.getBoundingClientRect();
|
|
60
|
-
return rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
|
|
61
27
|
}
|
|
@@ -15,20 +15,33 @@ function useCurrentSelection(choiceGroupName, defaultValue) {
|
|
|
15
15
|
// WARNING: We cannot use `keyPrefix` here: closures don't work because we serialize the function.
|
|
16
16
|
const initializeChoiceGroup_SSR = `initializeChoiceGroup();${initializeChoiceGroup.toString()};`;
|
|
17
17
|
function initializeChoiceGroup() {
|
|
18
|
-
const groupsElements =
|
|
18
|
+
const groupsElements = [
|
|
19
|
+
...document.querySelectorAll('select[data-choice-group]'),
|
|
20
|
+
...document.querySelectorAll('div[data-choice-group]'),
|
|
21
|
+
];
|
|
19
22
|
for (const groupEl of groupsElements) {
|
|
20
23
|
const choiceGroupName = groupEl.getAttribute('data-choice-group');
|
|
24
|
+
if (!choiceGroupName)
|
|
25
|
+
continue;
|
|
21
26
|
const storageKey = `docpress:choice:${choiceGroupName}`;
|
|
22
27
|
const selectedChoice = localStorage.getItem(storageKey);
|
|
23
|
-
if (selectedChoice)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
if (!selectedChoice)
|
|
29
|
+
continue;
|
|
30
|
+
switch (groupEl.tagName) {
|
|
31
|
+
case 'SELECT':
|
|
32
|
+
const selectEl = groupEl;
|
|
33
|
+
const optionExists = [...selectEl.options].some((opt) => opt.value === selectedChoice);
|
|
34
|
+
if (!optionExists)
|
|
35
|
+
localStorage.removeItem(storageKey);
|
|
36
|
+
else
|
|
37
|
+
selectEl.value = selectedChoice;
|
|
38
|
+
break;
|
|
39
|
+
case 'DIV':
|
|
40
|
+
const radioEl = groupEl.querySelector(`input[type="radio"][value="${selectedChoice}"]`);
|
|
41
|
+
if (radioEl)
|
|
42
|
+
radioEl.checked = true;
|
|
43
|
+
default:
|
|
44
|
+
break;
|
|
32
45
|
}
|
|
33
46
|
}
|
|
34
47
|
}
|
|
@@ -67,8 +67,11 @@ const remarkChoiceGroup = () => {
|
|
|
67
67
|
await remarkDetype.call(this)(tree, file);
|
|
68
68
|
remarkPkgManager.call(this)(tree, file);
|
|
69
69
|
visit(tree, 'mdxJsxFlowElement', (node) => {
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
// Descend into non-container nodes so that a `CustomSelectsContainer` nested inside another JSX
|
|
71
|
+
// element (e.g. react-tabs `<Tabs>`/`<TabPanel>`, or a `<div>`) still gets visited and its
|
|
72
|
+
// `choiceGroupAll` attribute injected. (Returning 'skip' here would stop the descent.)
|
|
73
|
+
if (node.name !== 'ChoiceGroupContainer')
|
|
74
|
+
return;
|
|
72
75
|
const choiceGroupAll = [];
|
|
73
76
|
visit(node, 'mdxJsxFlowElement', (child) => {
|
|
74
77
|
if (child.name !== 'ChoiceGroup')
|
|
@@ -107,7 +107,7 @@ function generateChoiceGroupCode(choiceNodes, parent, hide = false) {
|
|
|
107
107
|
if (lvl === 0) {
|
|
108
108
|
return {
|
|
109
109
|
type: 'mdxJsxFlowElement',
|
|
110
|
-
name: '
|
|
110
|
+
name: 'ChoiceGroupContainer',
|
|
111
111
|
attributes: [],
|
|
112
112
|
children: [choiceGroupNode],
|
|
113
113
|
};
|