@brillout/docpress 0.16.42 → 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.
@@ -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-color: hsl(0, 0%, 75%) hsl(0, 0%, 72%) hsl(0, 0%, 72%) hsl(0, 0%, 75%);
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-select {
18
- background: #eee;
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: 5px;
27
- border-style: solid;
28
- border-color: var(--border-color);
35
+ border-radius: var(--border-radius);
36
+ width: 100%;
37
+ transition: var(--transition-select-list);
29
38
  }
30
39
 
31
- .choice-select__list {
32
- position: relative;
33
- border-radius: 5px;
34
- border-color: var(--border-color);
35
- width: 100%;
36
- overflow: hidden;
37
- transition: top 180ms cubic-bezier(0.2, 0.9, 0.2, 1);
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: background 120ms ease;
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
- --transition: 500ms ease;
65
- transition: filter var(--transition), opacity var(--transition);
81
+ transition: var(--transition-select-icon);
66
82
  }
67
83
 
68
- .choice-select:hover .choice-select__option img,
69
- .choice-select[aria-expanded='true'] .choice-select__option img {
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-select[aria-expanded='false'] {
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
- .choice-select__list {
85
- border-style: solid;
86
- border-width: 1px 2px 2px 1px;
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
- border-bottom: none;
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[aria-selected='true'] {
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, CustomSelectsContainer }
1
+ export { ChoiceGroup, ChoiceGroupContainer }
2
2
 
3
3
  import type { ChoiceGroup as TChoiceGroup, ChoiceGroupWithParent } from '../types.js'
4
- import React, { createContext, useContext, useState } from '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
- const CustomSelectsContainerContext = createContext<{ choiceGroupAll: ChoiceGroupWithParent[] } | undefined>(undefined)
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
- return <CustomSelectsContainerContext value={{ choiceGroupAll }}>{children}</CustomSelectsContainerContext>
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, lvl } = choiceGroup
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 data-choice-group={groupName} data-lvl={lvl} className="choice-group">
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
- className={cls(['choice-select', (isHidden || isEmptyChoice(selectedChoice)) && 'hidden'])}
75
- style={{ height: OPTION_HEIGHT }}
76
- onMouseEnter={() => setExpanded(true)}
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
- aria-activedescendant={`choice-${selectedChoice}`}
84
- role="listbox"
85
- className="choice-select__list"
86
- style={{ top: rectTop, height: filteredChoices.length * OPTION_HEIGHT }}
87
- >
88
- {filteredChoices.map(({ name: choice, icon, iconStyle }, i) => (
89
- <div
90
- id={`choice-${choice}`}
91
- key={choice}
92
- aria-selected={i === selectedIndex}
93
- role="option"
94
- className="choice-select__option"
95
- style={{ height: OPTION_HEIGHT }}
96
- onClick={(e) => handleOnClick(e, choice)}
97
- >
98
- <span className="choice-select__option-content">
99
- <img src={icon} alt="" aria-hidden="true" style={iconStyle} />
100
- {choice}
101
- </span>
102
- </div>
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<HTMLDivElement, MouseEvent>, choice: string) {
113
- e.stopPropagation()
119
+ function handleOnClick(e: React.MouseEvent<HTMLLabelElement, MouseEvent>, choice: string) {
120
+ e.preventDefault()
114
121
  const el = e.currentTarget
115
122
  setPrevPosition(el)
116
- if (el.getAttribute('aria-selected') === 'true') {
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
- select:has(option:nth-of-type(1):checked) ~ ul[role='tablist'] li:nth-of-type(1),
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: 2px;
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" data-choice-group={groupName}>
25
- {/* Hidden select used to control tablist styling via CSS. */}
26
- <select name={`choicesFor-${groupName}`} value={selectedChoice} hidden disabled>
27
- {choices.map(({ name: choice }) => (
28
- <option key={choice} value={choice}>
29
- {choice}
30
- </option>
31
- ))}
32
- </select>
33
- <ul id={`choicesFor-${groupName}`} className="choice-tabs__tab-list" role="tablist">
34
- {choices.map(({ name: choice, icon, iconStyle }, i) => (
35
- <li
36
- key={choice}
37
- id={`tab-${choice}`}
38
- style={{ display: isHidden(choice) ? 'none' : undefined }}
39
- className="choice-tabs__tab"
40
- role="tab"
41
- aria-selected={i === selectedIndex}
42
- tabIndex={i === selectedIndex ? 0 : -1}
43
- onClick={(e) => handleOnClick(e, choice)}
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
- </li>
46
+ </label>
51
47
  ))}
52
- </ul>
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 = document.querySelectorAll<HTMLDivElement>('[data-choice-group]')
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
- const selectEl = groupEl.querySelector<HTMLSelectElement>(`select[name="choicesFor-${choiceGroupName}"]`)!
29
- const selectedIndex = [...selectEl.options].findIndex((option) => option.value === selectedChoice)
30
- if (selectedIndex === -1) {
31
- localStorage.removeItem(storageKey)
32
- } else {
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, CustomSelectsContainer } from '../components/ChoiceGroup.js'
6
+ import { ChoiceGroup, ChoiceGroupContainer } from '../components/ChoiceGroup.js'
7
7
 
8
8
  const useMDXComponents: UseMdxComponents = () => {
9
9
  return {
10
10
  ChoiceGroup,
11
- CustomSelectsContainer,
11
+ ChoiceGroupContainer,
12
12
  pre: (props) => <Pre {...props} />,
13
13
  }
14
14
  }
@@ -86,7 +86,7 @@ const remarkChoiceGroup: Plugin<[], Root> = (): Transformer<Root> => {
86
86
  // Descend into non-container nodes so that a `CustomSelectsContainer` nested inside another JSX
87
87
  // element (e.g. react-tabs `<Tabs>`/`<TabPanel>`, or a `<div>`) still gets visited and its
88
88
  // `choiceGroupAll` attribute injected. (Returning 'skip' here would stop the descent.)
89
- if (node.name !== 'CustomSelectsContainer') return
89
+ if (node.name !== 'ChoiceGroupContainer') return
90
90
 
91
91
  const choiceGroupAll: ChoiceGroupWithParent[] = []
92
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: 'CustomSelectsContainer',
133
+ name: 'ChoiceGroupContainer',
134
134
  attributes: [],
135
135
  children: [choiceGroupNode],
136
136
  }
package/css/index.css CHANGED
@@ -8,3 +8,4 @@
8
8
  @import './table.css';
9
9
  @import './tooltip.css';
10
10
  @import '@docsearch/css/dist/style.css';
11
+ @import './sr-only.css';
@@ -0,0 +1,11 @@
1
+ .sr-only {
2
+ position: absolute;
3
+ width: 1px;
4
+ height: 1px;
5
+ padding: 0;
6
+ margin: -1px;
7
+ overflow: hidden;
8
+ clip: rect(0, 0, 0, 0);
9
+ white-space: nowrap;
10
+ border: 0;
11
+ }
@@ -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
- const filteredChoices = choices.filter((choice) => !isHidden(choice.name));
18
- const selectedIndex = filteredChoices.findIndex((choice) => choice.name === selectedChoice);
19
- return (React.createElement("div", { className: "choice-tabs", "data-choice-group": groupName },
20
- React.createElement("select", { name: `choicesFor-${groupName}`, value: selectedChoice, hidden: true, disabled: true }, choices.map(({ name: choice }) => (React.createElement("option", { key: choice, value: choice }, choice)))),
21
- React.createElement("ul", { id: `choicesFor-${groupName}`, className: "choice-tabs__tab-list", role: "tablist" }, choices.map(({ name: choice, icon, iconStyle }, i) => (React.createElement("li", { key: choice, id: `tab-${choice}`, style: { display: isHidden(choice) ? 'none' : undefined }, className: "choice-tabs__tab", role: "tab", "aria-selected": i === selectedIndex, tabIndex: i === selectedIndex ? 0 : -1, onClick: (e) => handleOnClick(e, choice), onKeyDown: handleOnKeyDown },
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 = document.querySelectorAll('[data-choice-group]');
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
- const selectEl = groupEl.querySelector(`select[name="choicesFor-${choiceGroupName}"]`);
25
- const selectedIndex = [...selectEl.options].findIndex((option) => option.value === selectedChoice);
26
- if (selectedIndex === -1) {
27
- localStorage.removeItem(storageKey);
28
- }
29
- else {
30
- selectEl.value = selectedChoice;
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
  }
@@ -70,7 +70,7 @@ const remarkChoiceGroup = () => {
70
70
  // Descend into non-container nodes so that a `CustomSelectsContainer` nested inside another JSX
71
71
  // element (e.g. react-tabs `<Tabs>`/`<TabPanel>`, or a `<div>`) still gets visited and its
72
72
  // `choiceGroupAll` attribute injected. (Returning 'skip' here would stop the descent.)
73
- if (node.name !== 'CustomSelectsContainer')
73
+ if (node.name !== 'ChoiceGroupContainer')
74
74
  return;
75
75
  const choiceGroupAll = [];
76
76
  visit(node, 'mdxJsxFlowElement', (child) => {
@@ -107,7 +107,7 @@ function generateChoiceGroupCode(choiceNodes, parent, hide = false) {
107
107
  if (lvl === 0) {
108
108
  return {
109
109
  type: 'mdxJsxFlowElement',
110
- name: 'CustomSelectsContainer',
110
+ name: 'ChoiceGroupContainer',
111
111
  attributes: [],
112
112
  children: [choiceGroupNode],
113
113
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brillout/docpress",
3
- "version": "0.16.42",
3
+ "version": "0.16.43",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "@brillout/picocolors": "^1.0.10",