@apliteni/apliteni-ui 0.71.0 → 0.72.0
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/guidelines/accessibility-floor.md +28 -0
- package/package.json +1 -1
- package/react/dist/index.css +83 -0
- package/src/index.css +3 -0
- package/src/inline.js +4 -0
- package/src/styles/tap-zone.css +167 -0
|
@@ -14,6 +14,34 @@
|
|
|
14
14
|
|
|
15
15
|
**Except:** WCAG 2.5.8 permits spacing, equivalent controls, inline text, user-agent sizes and essential presentation. Overlays must not reach neighbours.
|
|
16
16
|
|
|
17
|
+
## Tap target on a phone
|
|
18
|
+
|
|
19
|
+
<!-- rule: tap-zone -->
|
|
20
|
+
|
|
21
|
+
**Rule:** Below the phone step, reach 44x44 with a transparent layer outside the control, never by growing the control.
|
|
22
|
+
|
|
23
|
+
**Why:** A finger covers more than a cursor, and a control that grows on a phone moves the page a reader already knows.
|
|
24
|
+
|
|
25
|
+
**Do:** Centre a transparent layer on the control and let it stop at half the gap to the nearest neighbour.
|
|
26
|
+
|
|
27
|
+
**Don't:** Grow the drawn control to 44 and push out every row that holds one.
|
|
28
|
+
|
|
29
|
+
**Except:** Only a coarse pointer gets the layer, or a cursor lights a control it is not over. Rows that share an edge — a menu, a dense table, a settings list — have nothing to grow into and stay as they are drawn. Fields generate no layer, so a field reaches the floor by its own height or not at all.
|
|
30
|
+
|
|
31
|
+
## Space between small targets
|
|
32
|
+
|
|
33
|
+
<!-- rule: tap-spacing -->
|
|
34
|
+
|
|
35
|
+
**Rule:** Below the phone step, open a row of small controls to 44 minus their drawn size, so both tap zones fit between the drawn edges.
|
|
36
|
+
|
|
37
|
+
**Why:** A zone reaches half the gap before it crosses its neighbour's edge, so the gap is what decides whether either control reaches the floor.
|
|
38
|
+
|
|
39
|
+
**Do:** Open a row of 24px marks to 20px on a phone, where both zones fit and neither mark changes size.
|
|
40
|
+
|
|
41
|
+
**Don't:** Keep the desktop 8px and let the control written later in the markup win the overlap.
|
|
42
|
+
|
|
43
|
+
**Except:** A row that is already full opens downward only — widening it across makes the controls narrower, which is the one thing the floor must not do. Rows that share an edge — a menu, a dense table, a settings list — have nothing to open and keep their drawn targets. A row that opens nothing grows no zone at all.
|
|
44
|
+
|
|
17
45
|
## Focus ring contrast
|
|
18
46
|
|
|
19
47
|
<!-- rule: ring-contrast -->
|
package/package.json
CHANGED
package/react/dist/index.css
CHANGED
|
@@ -26,6 +26,89 @@
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/* ../src/styles/tap-zone.css */
|
|
30
|
+
:root {
|
|
31
|
+
--tap-min: 44px;
|
|
32
|
+
--tap-aa: 24px;
|
|
33
|
+
--tap-gap: var(--space-5, 20px);
|
|
34
|
+
--tap-clear-x: 0px;
|
|
35
|
+
--tap-clear-y: 0px;
|
|
36
|
+
}
|
|
37
|
+
@media (max-width: 560px) and (pointer: coarse) {
|
|
38
|
+
:where(.ui-seg button, .ui-tabs__tab, .ui-dropdown__trigger, .ui-toast__action, .ui-drawer__close, .ui-snippet__copy, .ui-footer__social) {
|
|
39
|
+
position: relative;
|
|
40
|
+
}
|
|
41
|
+
:where(.ui-btn, .ui-seg button, .ui-tabs__tab, .ui-dropdown__trigger, .ui-toast__action, .ui-drawer__close, .ui-snippet__copy, .ui-footer__social, .ui-switch)::after {
|
|
42
|
+
content: "";
|
|
43
|
+
position: absolute;
|
|
44
|
+
left: 50%;
|
|
45
|
+
top: 50%;
|
|
46
|
+
transform: translate(-50%, -50%);
|
|
47
|
+
width: min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-x)));
|
|
48
|
+
height: min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-y)));
|
|
49
|
+
}
|
|
50
|
+
.ui-check input::before,
|
|
51
|
+
.ui-toast__close::before {
|
|
52
|
+
width: max(var(--tap-aa), min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-x))));
|
|
53
|
+
height: max(var(--tap-aa), min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-y))));
|
|
54
|
+
}
|
|
55
|
+
.ui-seg {
|
|
56
|
+
row-gap: var(--tap-gap) !important;
|
|
57
|
+
--tap-clear-x: var(--space-1, 4px);
|
|
58
|
+
--tap-clear-y: var(--tap-gap);
|
|
59
|
+
}
|
|
60
|
+
.ui-tabs__list {
|
|
61
|
+
--tap-clear-x: 2px;
|
|
62
|
+
--tap-clear-y: var(--tap-gap);
|
|
63
|
+
}
|
|
64
|
+
.ui-pager__steps {
|
|
65
|
+
row-gap: var(--tap-gap) !important;
|
|
66
|
+
--tap-clear-x: var(--space-1, 4px);
|
|
67
|
+
--tap-clear-y: var(--tap-gap);
|
|
68
|
+
}
|
|
69
|
+
.ui-pager {
|
|
70
|
+
row-gap: var(--tap-gap) !important;
|
|
71
|
+
--tap-clear-y: var(--tap-gap);
|
|
72
|
+
}
|
|
73
|
+
.ui-filter-bar {
|
|
74
|
+
gap: var(--tap-gap) !important;
|
|
75
|
+
--tap-clear-x: var(--tap-gap);
|
|
76
|
+
--tap-clear-y: var(--tap-gap);
|
|
77
|
+
}
|
|
78
|
+
.ui-filter-bar__chip {
|
|
79
|
+
--tap-clear-x: 0px;
|
|
80
|
+
--tap-clear-y: 0px;
|
|
81
|
+
}
|
|
82
|
+
.ui-table__act .ui-btn + .ui-btn {
|
|
83
|
+
margin-inline-start: var(--tap-gap) !important;
|
|
84
|
+
}
|
|
85
|
+
.ui-table__act {
|
|
86
|
+
--tap-clear-x: var(--tap-gap);
|
|
87
|
+
--tap-clear-y: 0px;
|
|
88
|
+
}
|
|
89
|
+
.ui-toast {
|
|
90
|
+
row-gap: var(--tap-gap) !important;
|
|
91
|
+
--tap-clear-y: var(--tap-gap);
|
|
92
|
+
}
|
|
93
|
+
.ui-confirm__acts,
|
|
94
|
+
.ui-toolbar,
|
|
95
|
+
.ui-empty__actions,
|
|
96
|
+
.ui-sx__actions,
|
|
97
|
+
.ui-fbc__foot,
|
|
98
|
+
.ui-drawer__footer,
|
|
99
|
+
.ui-snippet__bar,
|
|
100
|
+
.ui-app__main .ui-card__row {
|
|
101
|
+
row-gap: var(--tap-gap) !important;
|
|
102
|
+
--tap-clear-y: var(--tap-gap);
|
|
103
|
+
}
|
|
104
|
+
.ui-footer__socials,
|
|
105
|
+
.ui-drawer__header {
|
|
106
|
+
gap: var(--tap-gap) !important;
|
|
107
|
+
--tap-clear-x: var(--tap-gap);
|
|
108
|
+
--tap-clear-y: var(--tap-gap);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
29
112
|
/* ../src/styles/input.css */
|
|
30
113
|
.ui-field {
|
|
31
114
|
display: block;
|
package/src/index.css
CHANGED
|
@@ -44,3 +44,6 @@
|
|
|
44
44
|
@import "./styles/feedback.css";
|
|
45
45
|
@import "./styles/success.css";
|
|
46
46
|
@import "./styles/loading.css";
|
|
47
|
+
/* Last, and that is the point: the tap zone opens a gap a component sheet set,
|
|
48
|
+
so it has to be read after every one of them. why: specification, the tap floor */
|
|
49
|
+
@import "./styles/tap-zone.css";
|
package/src/inline.js
CHANGED
|
@@ -30,6 +30,7 @@ export const styles = {
|
|
|
30
30
|
base: baseCss,
|
|
31
31
|
reducedMotion: read('styles/reduced-motion.css'),
|
|
32
32
|
fieldZoom: read('styles/field-zoom.css'),
|
|
33
|
+
tapZone: read('styles/tap-zone.css'),
|
|
33
34
|
motion: read('styles/motion.css'),
|
|
34
35
|
button: read('styles/button.css'),
|
|
35
36
|
card: read('styles/card.css'),
|
|
@@ -92,4 +93,7 @@ export const cssText = [
|
|
|
92
93
|
styles.feedback,
|
|
93
94
|
styles.success,
|
|
94
95
|
styles.loading,
|
|
96
|
+
// Last in the list for the reason src/index.css imports it last: it opens a
|
|
97
|
+
// gap a component sheet set, so it is read after every one of them.
|
|
98
|
+
styles.tapZone,
|
|
95
99
|
].join('\n');
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/* apliteni-ui — the tap zone. Below the phone step, to a coarse pointer, a
|
|
2
|
+
* control that draws smaller than --tap-min carries a transparent layer that
|
|
3
|
+
* reaches the floor, and no control is drawn any bigger. Zones alone do not
|
|
4
|
+
* finish it: a layer takes HALF the clear space its container declares, so
|
|
5
|
+
* where the kit packs small controls closer than that, the GAP opens to
|
|
6
|
+
* --tap-gap. The controls keep their size; the space between them grows.
|
|
7
|
+
*
|
|
8
|
+
* Read LAST, after every component sheet — see src/index.css — because each
|
|
9
|
+
* opening overrides a gap one of them already set.
|
|
10
|
+
*
|
|
11
|
+
* why: docs/specification.md#a-tap-reaches-the-floor-below-the-phone-step
|
|
12
|
+
* Held by stories/tap-zone.test.js, measured in a browser at 390.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
:root {
|
|
16
|
+
/* WCAG 2.5.5, and where iOS and Android land. Not on the spacing scale,
|
|
17
|
+
which runs 40 then 48, because a fingertip is not spacing. Declared here
|
|
18
|
+
rather than in tokens.css for the reason field-zoom.css carries its own 16:
|
|
19
|
+
react/src/index.ts imports this sheet without the token file, and a var()
|
|
20
|
+
resolving to nothing there would size the layer to `auto` in silence. */
|
|
21
|
+
--tap-min: 44px;
|
|
22
|
+
|
|
23
|
+
/* WCAG 2.5.8 (AA), the target the kit holds at every width: the floor this
|
|
24
|
+
sheet raises FROM and may never drop below. */
|
|
25
|
+
--tap-aa: 24px;
|
|
26
|
+
|
|
27
|
+
/* 44 − 24: two zones fit between two of the smallest marks the kit draws. */
|
|
28
|
+
--tap-gap: var(--space-5, 20px);
|
|
29
|
+
|
|
30
|
+
/* Clear space to the nearest neighbour, declared by the container that knows
|
|
31
|
+
its own gap; a zone takes half on each side.
|
|
32
|
+
|
|
33
|
+
ZERO by default, so a zone grows nowhere until a container has opened the
|
|
34
|
+
room — assuming --tap-gap everywhere was measured handing `Keep the
|
|
35
|
+
workspace` a pixel of its own box that answered for `Delete it
|
|
36
|
+
permanently`. */
|
|
37
|
+
--tap-clear-x: 0px;
|
|
38
|
+
--tap-clear-y: 0px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@media (max-width: 560px) and (pointer: coarse) {
|
|
42
|
+
/* The containing block for every layer below. `.ui-btn` already has it; the
|
|
43
|
+
rest take it only inside this query, because `position: relative` lifts a
|
|
44
|
+
control over the in-flow one beside it and that changes who wins a shared
|
|
45
|
+
pixel. */
|
|
46
|
+
:where(
|
|
47
|
+
.ui-seg button,
|
|
48
|
+
.ui-tabs__tab,
|
|
49
|
+
.ui-dropdown__trigger,
|
|
50
|
+
.ui-toast__action,
|
|
51
|
+
.ui-drawer__close,
|
|
52
|
+
.ui-snippet__copy,
|
|
53
|
+
.ui-footer__social
|
|
54
|
+
) { position: relative; }
|
|
55
|
+
|
|
56
|
+
/* The layer: centred, transparent, sized to the floor, clamped to half the
|
|
57
|
+
clearance. */
|
|
58
|
+
:where(
|
|
59
|
+
.ui-btn,
|
|
60
|
+
.ui-seg button,
|
|
61
|
+
.ui-tabs__tab,
|
|
62
|
+
.ui-dropdown__trigger,
|
|
63
|
+
.ui-toast__action,
|
|
64
|
+
.ui-drawer__close,
|
|
65
|
+
.ui-snippet__copy,
|
|
66
|
+
.ui-footer__social,
|
|
67
|
+
.ui-switch
|
|
68
|
+
)::after {
|
|
69
|
+
content: "";
|
|
70
|
+
position: absolute;
|
|
71
|
+
left: 50%;
|
|
72
|
+
top: 50%;
|
|
73
|
+
transform: translate(-50%, -50%);
|
|
74
|
+
width: min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-x)));
|
|
75
|
+
height: min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-y)));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* ::after on these is spoken for — the tick and the dot, the toast close's
|
|
79
|
+
glyph plate — so the existing ::before grows, floored at --tap-aa. The
|
|
80
|
+
floor is load-bearing: `100%` is the PADDING box, 16 on a 19px checkbox
|
|
81
|
+
with a 1.5px border, so without it the clamp takes this layer DOWN to 22
|
|
82
|
+
and under the kit's own minimum. */
|
|
83
|
+
.ui-check input::before,
|
|
84
|
+
.ui-toast__close::before {
|
|
85
|
+
width: max(var(--tap-aa), min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-x))));
|
|
86
|
+
height: max(var(--tap-aa), min(max(100%, var(--tap-min)), calc(100% + var(--tap-clear-y))));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* -- Where the kit opens a gap, and where it cannot --------------------
|
|
90
|
+
* `!important` for the reason field-zoom.css takes it: each opening
|
|
91
|
+
* overrides a `gap` shorthand a component sheet set, and the React bundle
|
|
92
|
+
* ships this sheet without those sheets, where order is the consumer's. */
|
|
93
|
+
|
|
94
|
+
/* A strip keeps its pills packed across — that tight track IS the control —
|
|
95
|
+
and opens down, where the room is free until it wraps. */
|
|
96
|
+
.ui-seg {
|
|
97
|
+
row-gap: var(--tap-gap) !important;
|
|
98
|
+
--tap-clear-x: var(--space-1, 4px);
|
|
99
|
+
--tap-clear-y: var(--tap-gap);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Tabs are wide enough across; down, the panel under them has the room. */
|
|
103
|
+
.ui-tabs__list { --tap-clear-x: 2px; --tap-clear-y: var(--tap-gap); }
|
|
104
|
+
|
|
105
|
+
/* Equal cells read as one strip, so they stay packed across and open down,
|
|
106
|
+
which is where this strip wraps at this width. */
|
|
107
|
+
.ui-pager__steps {
|
|
108
|
+
row-gap: var(--tap-gap) !important;
|
|
109
|
+
--tap-clear-x: var(--space-1, 4px);
|
|
110
|
+
--tap-clear-y: var(--tap-gap);
|
|
111
|
+
}
|
|
112
|
+
.ui-pager { row-gap: var(--tap-gap) !important; --tap-clear-y: var(--tap-gap); }
|
|
113
|
+
|
|
114
|
+
/* Chips open both ways: the row wraps, and the button at the end of it is a
|
|
115
|
+
target like the chips are. */
|
|
116
|
+
.ui-filter-bar { gap: var(--tap-gap) !important; --tap-clear-x: var(--tap-gap); --tap-clear-y: var(--tap-gap); }
|
|
117
|
+
|
|
118
|
+
/* Inside a chip there is nothing to open: parting the value from its remove
|
|
119
|
+
mark would draw two controls where the reader sees one. Named on the
|
|
120
|
+
Accessibility minimums page. */
|
|
121
|
+
.ui-filter-bar__chip { --tap-clear-x: 0px; --tap-clear-y: 0px; }
|
|
122
|
+
|
|
123
|
+
/* Row actions open across — the pair #488 names — and cannot open down:
|
|
124
|
+
a dense table's rows share an edge, and parting them is the row height the
|
|
125
|
+
guideline protects. Down, they get the row's own padding, twice. */
|
|
126
|
+
/* Row actions — the pair #488 names — part ACROSS with a margin: the cell is
|
|
127
|
+
a `table-cell`, where `gap` does nothing, and declaring it anyway left the
|
|
128
|
+
mark's zone 9px inside `Revoke`. Nothing down, because a dense table's
|
|
129
|
+
rows share an edge and its padding is not clearance to spend. */
|
|
130
|
+
.ui-table__act .ui-btn + .ui-btn { margin-inline-start: var(--tap-gap) !important; }
|
|
131
|
+
.ui-table__act { --tap-clear-x: var(--tap-gap); --tap-clear-y: 0px; }
|
|
132
|
+
|
|
133
|
+
/* Down only, like the nine rows below: a toast is a full row at this width,
|
|
134
|
+
and opening it across took 24px out of the message for no reach at all —
|
|
135
|
+
its action already clears the floor on the vertical half. The box does not
|
|
136
|
+
open either: a toast clips to its own rounded corners for the timer bar,
|
|
137
|
+
and widening its padding grows the surface rather than the space between
|
|
138
|
+
controls, measured at four marks for 8px on every toast. */
|
|
139
|
+
.ui-toast { row-gap: var(--tap-gap) !important; --tap-clear-y: var(--tap-gap); }
|
|
140
|
+
|
|
141
|
+
/* The kit's remaining control rows open DOWN only. A row of labelled buttons
|
|
142
|
+
at 390 is often already full, so widening its column gap does not move the
|
|
143
|
+
buttons apart — it makes them narrower, and that is the half #488 rules
|
|
144
|
+
out. Measured on a toolbar of three buttons and a dropdown. These rows
|
|
145
|
+
wrap, which is also where the review measured them colliding. */
|
|
146
|
+
.ui-confirm__acts,
|
|
147
|
+
.ui-toolbar,
|
|
148
|
+
.ui-empty__actions,
|
|
149
|
+
.ui-sx__actions,
|
|
150
|
+
.ui-fbc__foot,
|
|
151
|
+
.ui-drawer__footer,
|
|
152
|
+
.ui-snippet__bar,
|
|
153
|
+
.ui-app__main .ui-card__row {
|
|
154
|
+
row-gap: var(--tap-gap) !important;
|
|
155
|
+
--tap-clear-y: var(--tap-gap);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Both ways: fixed squares cannot overflow the way labels can, and a drawer
|
|
159
|
+
header spends its column gap on the title. Not the accent picker — its
|
|
160
|
+
swatches are on no carrier list, so opening it bought no reach. */
|
|
161
|
+
.ui-footer__socials,
|
|
162
|
+
.ui-drawer__header {
|
|
163
|
+
gap: var(--tap-gap) !important;
|
|
164
|
+
--tap-clear-x: var(--tap-gap);
|
|
165
|
+
--tap-clear-y: var(--tap-gap);
|
|
166
|
+
}
|
|
167
|
+
}
|