@pastweb/components-core 2.0.0 → 2.2.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.
@@ -0,0 +1,120 @@
1
+ # RangeSlider
2
+
3
+ RangeSlider selects one numeric value from a bounded range using the native
4
+ range control. Its rendering contract follows the WAI-ARIA Slider pattern and
5
+ targets WCAG 2.2 Level AA.
6
+
7
+ ## When to use
8
+
9
+ Use it when an approximate value is acceptable and the range is meaningful. Use
10
+ a numeric input when users must enter an exact value. Always supply a visible
11
+ `label` or a specific `ariaLabel`.
12
+
13
+ ## Props
14
+
15
+ | PropName | Type | Required | Default | Description |
16
+ |---|---|---:|---:|---|
17
+ | `cls` | `(...args: ClassValue[]) => string` | yes | - | Resolves component, part, and state classes. |
18
+ | `ariaLabel` | `string` | no | `"Range value"` | Names the slider when no visible label is supplied. Pass an empty string to omit the name. |
19
+ | `id` | `string` | no | generated | Stable native input ID. Generated IDs use the `rangeSlider` prefix. Explicit IDs are stored as `_${id}`. |
20
+ | `label` | `string` | no | `""` | Visible native label. |
21
+ | `labelPlacement` | aligned placement | no | `'top-start'` | Label placement. |
22
+ | `min` | `number` | yes | - | Minimum. |
23
+ | `max` | `number` | yes | - | Maximum. |
24
+ | `step` | `number` | no | browser default | Increment. |
25
+ | `value` | `number` | no | range midpoint | Current value. |
26
+ | `disabled` | `boolean` | no | - | Disables the native input and applies `is-disabled`. |
27
+ | `onChange` | `(value, event) => void` | no | noop | Reports each value change. |
28
+ | `onMouseUp` | `(value) => void` | no | noop | Compatibility callback reporting committed pointer or keyboard value. |
29
+ | `className` | `string` | no | `""` | Additional root classes. |
30
+
31
+ ## Mediator state
32
+
33
+ | Property | Type | Description |
34
+ |---|---|---|
35
+ | `ariaLabel` | `string \| undefined` | Normalized accessible name when no visible `label` is present. Empty labels are omitted. |
36
+ | `inputId` | `string` | Native input ID from an explicit `id` or a generated `rangeSlider` prefix. |
37
+ | `isDisabled` | `true \| undefined` | Is `true` when the slider is disabled; otherwise omitted. Renderers use this value directly for `.is-disabled` and native `disabled`. |
38
+ | `value` | `string` | Clamped current value as a string for the native range input. |
39
+
40
+ ## Utility classes
41
+
42
+ Apply these classes through `className`.
43
+
44
+ | Class | Description |
45
+ |---|---|
46
+ | `is-full-width` | Expands the slider to the available width. |
47
+
48
+ ## CSS Variables
49
+
50
+ | Variable | Part | State | CSS property | Default | Description | Platforms |
51
+ |---|---|---|---|---|---|---|
52
+ | `--pw_RangeSlider_root_default_margin` | `root` | `default` | `margin` | shared margins | Component spacing. | Web, Capacitor, NativeScript |
53
+ | `--pw_RangeSlider_root_disabled_opacity` | `root` | `disabled` | `opacity` | `var(--pw_disabled_opacity)` | Disabled appearance. | Web, Capacitor, NativeScript |
54
+ | `--pw_RangeSlider_track_default_height` | `track` | `default` | `height` | `0.8em` | Track height. | Web, Capacitor, NativeScript |
55
+ | `--pw_RangeSlider_track_default_background-color` | `track` | `default` | `background-color` | `var(--pw_control_background-color)` | Track background. | Web, Capacitor, NativeScript |
56
+ | `--pw_RangeSlider_track_default_border-color` | `track` | `default` | `border-color` | `var(--pw_border-color)` | Track and thumb border. | Web, Capacitor, NativeScript |
57
+ | `--pw_RangeSlider_track_default_border-radius` | `track` | `default` | `border-radius` | `var(--pw_border-radius)` | Track corners. | Web, Capacitor, NativeScript |
58
+ | `--pw_RangeSlider_track_default_border-style` | `track` | `default` | `border-style` | `var(--pw_border-style)` | Track and thumb border style. | Web, Capacitor, NativeScript |
59
+ | `--pw_RangeSlider_track_default_border-width` | `track` | `default` | `border-width` | `var(--pw_border-width)` | Track and thumb border width. | Web, Capacitor, NativeScript |
60
+ | `--pw_RangeSlider_thumb_default_width` | `thumb` | `default` | `width` | `1.562em` | Pointer target width. | Web, Capacitor, NativeScript |
61
+ | `--pw_RangeSlider_thumb_default_height` | `thumb` | `default` | `height` | `1.562em` | Pointer target height. | Web, Capacitor, NativeScript |
62
+ | `--pw_RangeSlider_thumb_default_background-color` | `thumb` | `default` | `background-color` | track background | Thumb background. | Web, Capacitor, NativeScript |
63
+ | `--pw_RangeSlider_thumb_default_border-radius` | `thumb` | `default` | `border-radius` | `50%` | Thumb shape. | Web, Capacitor, NativeScript |
64
+ | `--pw_RangeSlider_thumb_default_transition-duration` | `thumb` | `default` | `transition-duration` | `var(--pw_transition-duration)` | Thumb visual duration. | Web, Capacitor; NativeScript adapter |
65
+ | `--pw_RangeSlider_label_default_color` | `label` | `default` | `color` | `var(--pw_label_text_color)` | Label colour. | Web, Capacitor, NativeScript |
66
+ | `--pw_RangeSlider_label_default_font-size` | `label` | `default` | `font-size` | `var(--pw_font-size)` | Label font size. | Web, Capacitor, NativeScript |
67
+ | `--pw_RangeSlider_label_default_font-weight` | `label` | `default` | `font-weight` | `var(--pw_semibold_font-weight)` | Label font weight. | Web, Capacitor, NativeScript |
68
+ | `--pw_RangeSlider_label_default_line-height` | `label` | `default` | `line-height` | `var(--pw_line-height)` | Label line height. | Web, Capacitor, NativeScript |
69
+
70
+ ```xts
71
+ import { useBeforeUnmount, useMediator, withDefaultProps } from '@pastweb/x';
72
+ import { rangeSliderMediator } from '@pastweb/components-core/RangeSlider';
73
+ import { DEFAULT_PROPS } from './constants';
74
+
75
+ export function RangeSlider(props: RangeSliderProps) {
76
+ const p = withDefaultProps<RangeSliderDefaultProps>(props, DEFAULT_PROPS);
77
+ const m = useMediator<RangeSliderMediator>(rangeSliderMediator, p);
78
+ const s = m.state;
79
+
80
+ useBeforeUnmount(m.useBeforeUnmount);
81
+
82
+ return (
83
+ <div class={p.cls('RangeSlider', `label-${p.labelPlacement}`, { 'is-disabled': s.isDisabled }, p.className)}>
84
+ <input class={p.cls('slider')} type="range" id={s.inputId} min={p.min} max={p.max}
85
+ step={p.step} value={s.value} disabled={s.isDisabled}
86
+ aria-label={s.ariaLabel}
87
+ @change={event => m.onChange(event.target.value, event)}
88
+ @pointerup={m.onChangeEnd} @keyup={m.onChangeEnd} />
89
+ <label #if={p.label} class={p.cls('label', p.labelPlacement)} for={s.inputId}>{p.label}</label>
90
+ </div>
91
+ );
92
+ }
93
+ ```
94
+
95
+ ## Keyboard interaction
96
+
97
+ - Arrow keys, `Home`, `End`, `PageUp`, and `PageDown` adjust the native range
98
+ value.
99
+ - `Tab` and `Shift+Tab` move sequential focus to the available slider.
100
+
101
+ ## Accessibility notes
102
+
103
+ - The native input supplies slider role, values, keyboard operation, and
104
+ disabled behavior.
105
+ - The visible label is connected through `for`/`id`. When it is absent,
106
+ `ariaLabel` names the slider. The default name is `Range value`.
107
+ - Disabled state is reflected through `.is-disabled` and native `disabled`.
108
+ - Shared web styles provide a visible `:focus-visible` indicator, forced-colours
109
+ treatment, and reduced-motion behavior.
110
+
111
+ ## Known limitations
112
+
113
+ - Automated accessibility checks do not by themselves establish WCAG
114
+ conformance.
115
+
116
+ ## Standards
117
+
118
+ - WCAG 2.2: https://www.w3.org/TR/WCAG22/
119
+ - WAI-ARIA Slider pattern: https://www.w3.org/WAI/ARIA/apg/patterns/slider/
120
+ - GOV.UK form controls: https://design-system.service.gov.uk/components/
@@ -0,0 +1,105 @@
1
+ # ScrollArea
2
+
3
+ ScrollArea provides a named, keyboard-focusable viewport for overflowing
4
+ content. Its rendering contract targets WCAG 2.2 Level AA.
5
+
6
+ ## When to use
7
+
8
+ Use it when content must remain within a bounded layout. Avoid nested scroll
9
+ areas and never hide an axis when that would make content unreachable.
10
+
11
+ ## Props
12
+
13
+ | PropName | Type | Required | Default | Description |
14
+ |---|---|---:|---:|---|
15
+ | `cls` | `(...args: ClassValue[]) => string` | yes | - | Resolves component and part classes. |
16
+ | `ariaLabel` | `string` | no | `"Scrollable content"` | Names the region. Pass an empty string to omit the name. Prefer a specific label. |
17
+ | `id` | `string` | no | - | Root ID. |
18
+ | `className` | `string` | no | `""` | Additional root classes. |
19
+ | `hideX` | `boolean` | no | - | Applies `hide-x` and suppresses horizontal overflow. |
20
+ | `hideY` | `boolean` | no | - | Applies `hide-y` and suppresses vertical overflow. |
21
+ | `scrollRight` | `boolean` | no | - | Applies `scroll-right` and requests initial horizontal end alignment. |
22
+ | `scrollBottom` | `boolean` | no | - | Applies `scroll-bottom` and requests initial vertical end alignment. |
23
+ | `contentPadding` | `number \| string` | no | - | Inline padding scoped to `.content`. |
24
+ | `tabIndex` | `number` | no | `0` | Makes the scroll viewport keyboard focusable. |
25
+
26
+ The default slot supplies scrollable content.
27
+
28
+ ## Mediator state
29
+
30
+ | Property | Type | Description |
31
+ |---|---|---|
32
+ | `ariaLabel` | `string \| undefined` | Normalized region name. Empty labels are omitted. |
33
+
34
+ `getContentStyle()` returns `{ padding }` when `contentPadding` is provided,
35
+ including `0`. `getScrollAlignment()` returns semantic `start` or `end` axes
36
+ for the renderer to apply after layout.
37
+
38
+ ## CSS Variables
39
+
40
+ | Variable | Part | State | CSS property | Default | Description | Platforms |
41
+ |---|---|---|---|---|---|---|
42
+ | `--pw_ScrollArea_content_default_scrollbar-color` | `content` | `default` | `scrollbar-color` | `#e3e3e3 transparent` | Standards scrollbar colours. | Web, Capacitor |
43
+ | `--pw_ScrollArea_content_default_scrollbar-width` | `content` | `default` | `scrollbar-width` | `thin` | Standards scrollbar width. | Web, Capacitor |
44
+ | `--pw_ScrollArea_scrollbar_default_width` | `scrollbar` | `default` | `width` | `12px` | WebKit vertical width. | Web, Capacitor |
45
+ | `--pw_ScrollArea_scrollbar_default_height` | `scrollbar` | `default` | `height` | `12px` | WebKit horizontal height. | Web, Capacitor |
46
+ | `--pw_ScrollArea_scrollbar-track_default_background-color` | `scrollbar-track` | `default` | `background-color` | `#fcfcfc` | Track colour. | Web, Capacitor |
47
+ | `--pw_ScrollArea_scrollbar-thumb_default_background-color` | `scrollbar-thumb` | `default` | `background-color` | `#dfdfdf` | Thumb colour. | Web, Capacitor |
48
+ | `--pw_ScrollArea_scrollbar-thumb_hover_background-color` | `scrollbar-thumb` | `hover` | `background-color` | `#7a7a7a` | Hover thumb colour. | Web, Capacitor |
49
+ | `--pw_ScrollArea_scrollbar-thumb_default_border-radius` | `scrollbar-thumb` | `default` | `border-radius` | `6px` | Thumb corners. | Web, Capacitor |
50
+ | `--pw_ScrollArea_scrollbar-corner_default_background-color` | `scrollbar-corner` | `default` | `background-color` | `#fbfbfb` | Corner colour. | Web, Capacitor |
51
+
52
+ ```xts
53
+ import { useMediator, useMounted, useRef, withDefaultProps } from '@pastweb/x';
54
+ import { scrollAreaMediator } from '@pastweb/components-core/ScrollArea';
55
+ import { DEFAULT_PROPS } from './constants';
56
+
57
+ export function ScrollArea(props: ScrollAreaProps) {
58
+ const p = withDefaultProps<ScrollAreaDefaultProps>(props, DEFAULT_PROPS);
59
+ const m = useMediator<ScrollAreaMediator>(scrollAreaMediator, p);
60
+ const s = m.state;
61
+ const content = useRef();
62
+
63
+ useMounted(() => {
64
+ const alignment = m.getScrollAlignment();
65
+ if (alignment.horizontal === 'end') content.value.scrollLeft = content.value.scrollWidth - content.value.clientWidth;
66
+ if (alignment.vertical === 'end') content.value.scrollTop = content.value.scrollHeight - content.value.clientHeight;
67
+ });
68
+
69
+ return (
70
+ <div id={p.id} class={p.cls('ScrollArea', {
71
+ 'hide-x': p.hideX, 'hide-y': p.hideY,
72
+ 'scroll-bottom': p.scrollBottom, 'scroll-right': p.scrollRight,
73
+ }, p.className)}>
74
+ <div #ref={content} class={p.cls('content')} #style={m.getContentStyle()}
75
+ role="region" aria-label={s.ariaLabel} tabindex={p.tabIndex}><slot /></div>
76
+ </div>
77
+ );
78
+ }
79
+ ```
80
+
81
+ ## Keyboard interaction
82
+
83
+ - `Tab` and `Shift+Tab` move sequential focus to the named viewport.
84
+ - Arrow keys, `PageUp`, `PageDown`, `Home`, and `End` scroll native overflow
85
+ when the viewport is focused.
86
+
87
+ ## Accessibility notes
88
+
89
+ - The web viewport is a named `region` and is keyboard focusable.
90
+ - Renderers consume `getScrollAlignment()` after layout instead of reversing
91
+ visual content order.
92
+ - Hiding an axis must not make content unreachable.
93
+ - Shared web styles provide a visible `:focus-visible` indicator.
94
+
95
+ ## Known limitations
96
+
97
+ - Nested scroll areas make keyboard scrolling harder to discover.
98
+ - Automated accessibility checks do not by themselves establish WCAG
99
+ conformance.
100
+
101
+ ## Standards
102
+
103
+ - WCAG 2.2: https://www.w3.org/TR/WCAG22/
104
+ - WAI-ARIA landmark regions: https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/
105
+ - GOV.UK components: https://design-system.service.gov.uk/components/
@@ -0,0 +1,243 @@
1
+ # Select
2
+
3
+ Select chooses one value from a listbox attached to a trigger. The trigger is
4
+ either a styled native button (`content`) or a custom control (`target`). Its
5
+ rendering contract follows the WAI-ARIA Combobox/Listbox pattern and targets
6
+ WCAG 2.2 Level AA.
7
+
8
+ ## When to use
9
+
10
+ Use Select when the user must pick one option from a known list. Do not use it
11
+ for free-text entry. Prefer a native radio group for a very small set of options
12
+ that should stay visible.
13
+
14
+ ## Props
15
+
16
+ | PropName | Type | Required | Default | Description |
17
+ |---|---|---:|---:|---|
18
+ | `cls` | `(...args: ClassValue[]) => string` | yes | - | Resolves component and state classes. |
19
+ | `id` | `string` | no | generated | Supplies the host ID. Generated IDs use the `select` prefix. |
20
+ | `ariaLabel` | `string` | no | `"Select an option"` | Names the trigger when there is no visible `label`. Pass an empty string to omit the accessible name. |
21
+ | `name` | `string` | no | - | Optional form name. |
22
+ | `label` | `string` | no | `""` | Visible label. When set, it names the trigger and `ariaLabel` is omitted. |
23
+ | `labelPlacement` | `Placement` | no | `"top-start"` | Places the visible label relative to the trigger. |
24
+ | `placeholder` | `string` | no | `"Select"` | Default text of the styled `content` trigger when that slot is empty. |
25
+ | `disabled` | `boolean` | no | - | Makes the Select unavailable. |
26
+ | `loading` | `boolean` | no | - | Marks the Select busy and unavailable. |
27
+ | `className` | `string` | no | `""` | Adds classes to the root. The `is-loading` class also enables loading. |
28
+ | `tabIndex` | `number` | no | `0` | Controls sequential keyboard focus order. |
29
+ | `value` | `any` | no | - | Controlled selected value. |
30
+ | `offset` | `number` | no | `3` | Listbox offset from the trigger. |
31
+ | `closeOnSelect` | `boolean` | no | `true` | Closes the listbox after a selection. |
32
+ | `hideOnClick` | `boolean` | no | `true` | Closes the listbox after activation inside the options. |
33
+ | `onChange` | `(value: any) => void` | no | noop | Reports the selected value. |
34
+
35
+ ## Utility classes
36
+
37
+ Apply these classes through `className`.
38
+
39
+ | Class | Description |
40
+ |---|---|
41
+ | `is-rounded` | Uses the rounded radius token on `.selectButton` and the listbox. Custom `target` triggers are not styled by this class. |
42
+ | `is-full-width` | Expands the Select to the available width. |
43
+
44
+ ## Mediator API
45
+
46
+ | Property | Type | Description |
47
+ |---|---|---|
48
+ | `id` | `string` | Resolved host ID. Generated IDs use the `select` prefix; an explicit `id` is exposed as `_${id}`. |
49
+ | `hide` | `() => void` | Closes the listbox. |
50
+
51
+ ## Mediator state
52
+
53
+ | Property | Type | Description |
54
+ |---|---|---|
55
+ | `ariaLabel` | `string \| undefined` | Normalized trigger accessible name. Omitted when `label` is set or `ariaLabel` is empty. |
56
+ | `highlightIndex` | `number \| null` | Keyboard highlight index in the listbox. |
57
+ | `inputId` | `string` | Root ID. Same value as mediator `id`. |
58
+ | `isDisabled` | `true \| undefined` | Is `true` when the Select is disabled; otherwise omitted. |
59
+ | `isLoading` | `true \| undefined` | Is `true` when the Select is loading; otherwise omitted. |
60
+ | `isOpen` | `boolean` | Whether the listbox is open. |
61
+ | `isUnavailable` | `true \| undefined` | Is `true` when disabled or loading. |
62
+ | `labelId` | `string` | Visible-label relationship ID in the form `label${id}`. |
63
+ | `listboxId` | `string` | Listbox relationship ID in the form `listbox${id}`. |
64
+ | `triggerId` | `string` | Trigger relationship ID in the form `trigger${id}`. |
65
+ | `value` | `any` | Current selected value. |
66
+
67
+ ## Mediator context
68
+
69
+ SelectItem reads this API through the renderer context hook. Live getters stay synchronized with mediator state.
70
+
71
+ | Context property | Type | Description |
72
+ |---|---|---|
73
+ | `hide` | `() => void` | Closes the listbox. |
74
+ | `onChange` | `(value: any) => void` | Selects a value. |
75
+ | `onOptionKeyDown` | `(event, value, disabled?) => void` | Handles option keyboard activation. |
76
+ | `registerItem` | `() => number` | Registers a SelectItem and returns its index. |
77
+ | `state` | `SelectState` | Live mediator state for descendants. |
78
+
79
+ ## Slots
80
+
81
+ Provide `content` for the styled button trigger, or `target` for a custom
82
+ trigger. Each trigger is mounted only when its slot has content. If neither
83
+ slot is provided, no trigger is rendered. Do not put another button inside
84
+ either trigger slot.
85
+
86
+ | SlotName | Description | Default content |
87
+ |---|---|---|
88
+ | `target` | Replaces the native trigger. It receives the trigger ID, ARIA state, keyboard handler, toggle handler, and tab index through slot props; the replacement must apply them to one focusable control. | The native `content` trigger. |
89
+ | `content` | Labelled content of the styled native-button trigger (`.selectButton`), which includes a decorative arrow. When present, the button is rendered. | `placeholder` |
90
+ | `options` | Supplies listbox options such as `SelectItem`. | Empty. |
91
+ | default | Supplies additional listbox content. | Empty. |
92
+
93
+ ## CSS Variables
94
+
95
+ | Variable | Part | State | CSS property | Default | Description | Platforms |
96
+ |---|---|---|---|---|---|---|
97
+ | `--pw_Select_root_default_min-width` | `root` | `default` | `min-width` | `12em` | Minimum control width. The control can grow with trigger content. | Web, Capacitor, NativeScript |
98
+ | `--pw_Select_options_default_width` | `options` | `default` | `width` | `max-content` | Listbox width, sized to option content. | Web, Capacitor |
99
+ | `--pw_Select_options_default_max-height` | `options` | `default` | `max-height` | `16em` | Listbox max height; overflow scrolls vertically. | Web, Capacitor |
100
+ | `--pw_Select_root_default_background-color` | `root` | `default` | `background-color` | `var(--pw_control_background-color)` | Control and listbox background. | Web, Capacitor, NativeScript |
101
+ | `--pw_Select_root_default_color` | `root` | `default` | `color` | `var(--pw_text_strong_color)` | Control and option foreground. | Web, Capacitor, NativeScript |
102
+ | `--pw_Select_label_default_color` | `label` | `default` | `color` | `var(--pw_label_text_color)` | Visible label colour. | Web, Capacitor, NativeScript |
103
+ | `--pw_Select_root_default_border-color` | `root` | `default` | `border-color` | `var(--pw_border-color)` | Listbox border colour. | Web, Capacitor, NativeScript |
104
+ | `--pw_Select_root_default_border-width` | `root` | `default` | `border-width` | `var(--pw_border-width)` | Listbox border width. | Web, Capacitor, NativeScript |
105
+ | `--pw_Select_root_default_border-style` | `root` | `default` | `border-style` | `var(--pw_border-style)` | Listbox border style. | Web, Capacitor, NativeScript |
106
+ | `--pw_Select_root_default_border-radius` | `root` | `default` | `border-radius` | `var(--pw_border-radius)` | Listbox corner radius. | Web, Capacitor, NativeScript |
107
+ | `--pw_Select_root_default_padding` | `root` | `default` | `padding` | `var(--pw_padding)` | Option padding. | Web, Capacitor, NativeScript |
108
+ | `--pw_Select_root_default_margin` | `root` | `default` | `margin` | vertical/horizontal margin | Outer spacing. | Web, Capacitor, NativeScript |
109
+ | `--pw_Select_root_disabled_opacity` | `root` | `disabled` | `opacity` | `var(--pw_disabled_opacity)` | Disabled opacity. | Web, Capacitor, NativeScript |
110
+
111
+ ```xts
112
+ import {
113
+ setContext,
114
+ useBeforeUnmount,
115
+ useMediator,
116
+ useMounted,
117
+ useRef,
118
+ withDefaultProps,
119
+ } from '@pastweb/x';
120
+ import { selectMediator } from '@pastweb/components-core/Select';
121
+ import { DEFAULT_PROPS, SELECT_CONTEXT_KEY } from './constants';
122
+
123
+ export function Select(props: SelectProps) {
124
+ const p = withDefaultProps<SelectDefaultProps>(props, DEFAULT_PROPS);
125
+ const m = useMediator<SelectMediator>(selectMediator, p);
126
+ const s = m.state;
127
+ const c = m.getContext();
128
+ const triggerReference = useRef(null);
129
+ const listboxReference = useRef(null);
130
+
131
+ setContext(SELECT_CONTEXT_KEY, c);
132
+ useMounted(() => {
133
+ if (triggerReference.value && listboxReference.value) {
134
+ m.useMounted(triggerReference.value, listboxReference.value);
135
+ }
136
+ });
137
+ useBeforeUnmount(m.useBeforeUnmount);
138
+
139
+ return (
140
+ <div
141
+ id={s.inputId}
142
+ class={p.cls('Select', `label-${p.labelPlacement}`, p.className, {
143
+ 'is-disabled': s.isDisabled,
144
+ 'is-loading': s.isLoading,
145
+ 'is-open': s.isOpen,
146
+ })}
147
+ >
148
+ <div class={p.cls('selectControl')}>
149
+ <slot
150
+ name='target'
151
+ props={{
152
+ ariaBusy: s.isLoading || undefined,
153
+ ariaControls: s.listboxId,
154
+ ariaExpanded: s.isOpen,
155
+ ariaHasPopup: 'listbox',
156
+ ariaLabel: s.ariaLabel,
157
+ ariaLabelledBy: p.label ? s.labelId : undefined,
158
+ ariaDisabled: s.isUnavailable,
159
+ id: s.triggerId,
160
+ onKeyDown: m.onKeyDown,
161
+ onToggle: m.onToggle,
162
+ tabIndex: p.tabIndex,
163
+ }}
164
+ >
165
+ <button
166
+ #ref={triggerReference}
167
+ id={s.triggerId}
168
+ type='button'
169
+ class={p.cls('selectButton')}
170
+ tabIndex={p.tabIndex}
171
+ disabled={s.isUnavailable}
172
+ aria-haspopup='listbox'
173
+ aria-expanded={s.isOpen}
174
+ aria-controls={s.listboxId}
175
+ aria-labelledby={p.label ? s.labelId : undefined}
176
+ aria-label={s.ariaLabel}
177
+ aria-busy={s.isLoading || undefined}
178
+ @keydown={m.onKeyDown}
179
+ @click={m.onToggle}
180
+ >
181
+ <slot name='content'>{p.placeholder}</slot>
182
+ <span class={p.cls('arrow')} aria-hidden='true'>▼</span>
183
+ </button>
184
+ </slot>
185
+ <ul
186
+ #ref={listboxReference}
187
+ id={s.listboxId}
188
+ role='listbox'
189
+ class={p.cls('options')}
190
+ aria-labelledby={p.label ? s.labelId : s.triggerId}
191
+ hidden={!s.isOpen}
192
+ >
193
+ <slot name='options' />
194
+ <slot />
195
+ </ul>
196
+ </div>
197
+ <label #if={p.label} id={s.labelId} class={p.cls('label', p.labelPlacement)} for={s.triggerId}>
198
+ {p.label}
199
+ </label>
200
+ </div>
201
+ );
202
+ }
203
+ ```
204
+
205
+ ## Keyboard interaction
206
+
207
+ - `Enter` or `Space` on the trigger toggles the listbox.
208
+ - `ArrowDown` opens and highlights the first option, or moves down.
209
+ - `ArrowUp` opens and highlights the last option, or moves up.
210
+ - `Home` and `End` move to the first and last option while open.
211
+ - `Escape` closes an open listbox and restores trigger focus.
212
+ - `Tab` closes an open listbox and continues document order.
213
+
214
+ ## Accessibility notes
215
+
216
+ - Provide either a `content` trigger or a `target` trigger. Each trigger exposes
217
+ expanded, controls, disabled, and busy state.
218
+ - The `content` trigger is a native button with a decorative arrow. Do not place
219
+ another button in the `content` slot.
220
+ - A `target` replacement receives the required trigger semantics as slot props. Apply them to one focusable control and do not nest controls.
221
+ - Visible `label` text is the accessible name. `ariaLabel` is used only when there is no visible label.
222
+ - The visible `label` is associated with the `content` button through `for` and
223
+ `triggerId`. The `target` trigger is named with `aria-labelledby` when a
224
+ visible label is present.
225
+ - Disabled and loading states are unavailable and cannot open the listbox.
226
+ - Closed options use `hidden` so they leave the accessibility tree and tab order.
227
+ - The listbox is sized to option content and at least as wide as the trigger. It
228
+ flips only to the opposite edge when the viewport has no room below, and
229
+ scrolls vertically when options exceed the max height.
230
+ - Shared web styles provide a visible focus indicator, forced-colours treatment,
231
+ and reduced-motion behavior.
232
+
233
+ ## Known limitations
234
+
235
+ - Content-slot and target-slot content should not introduce a nested button.
236
+ - Listbox positioning uses web layout measurement.
237
+ - Automated accessibility checks do not by themselves establish WCAG conformance.
238
+
239
+ ## Standards
240
+
241
+ - WCAG 2.2: https://www.w3.org/TR/WCAG22/
242
+ - WAI-ARIA Combobox/Listbox pattern: https://www.w3.org/WAI/ARIA/apg/patterns/combobox/
243
+ - GOV.UK select: https://design-system.service.gov.uk/components/select/
@@ -0,0 +1,143 @@
1
+ # SheetController
2
+
3
+ SheetController presents an animated, optionally modal dialog surface from an
4
+ edge of its portal container. Its rendering contract follows the WAI-ARIA Dialog
5
+ (Modal) pattern and targets WCAG 2.2 Level AA.
6
+
7
+ ## When to use
8
+
9
+ Use it for secondary tasks that need more space than a popover without
10
+ navigating away. Do not use it for essential content that should remain
11
+ permanently visible. A modal sheet must include an obvious close control inside
12
+ its content.
13
+
14
+ ## Props
15
+
16
+ | PropName | Type | Required | Default | Description |
17
+ |---|---|---:|---:|---|
18
+ | `cls` | `(...args: ClassValue[]) => string` | yes | - | Resolves component and part classes. |
19
+ | `id` | `string` | no | generated | Supplies the dialog ID. Generated IDs use the `sheetController` prefix. Explicit IDs are stored as `_${id}`. |
20
+ | `ariaLabel` | `string` | no | `'Sheet'` | Accessible dialog name. Pass an empty string to omit the name. |
21
+ | `className` | `string` | no | `''` | Additional root classes. |
22
+ | `direction` | `'left' \| 'right' \| 'up' \| 'down'` | no | `'left'` | Entry and exit edge. |
23
+ | `backdrop` | `boolean` | no | - | Displays the native backdrop button and applies `has-backdrop`. |
24
+ | `modal` | `boolean` | no | - | Enables `aria-modal`, focus containment, and `is-modal`. |
25
+ | `closeOnBackdropClick` | `boolean` | no | `true` | Allows backdrop activation to close. |
26
+ | `closeOnEscape` | `boolean` | no | `true` | Allows Escape to close unless the event is prevented. |
27
+ | `animationTime` | `number` | no | `400` | Fallback close duration in milliseconds. |
28
+ | `width`, `height` | `number \| string` | no | - | Inline sheet dimensions. |
29
+ | `top`, `right`, `bottom`, `left` | `number \| string` | no | - | Inline sheet offsets. |
30
+ | `portal`, `portalElement`, `entryId` | portal integration | no | - | Optional host integration used for update/remove/close events. |
31
+ | `onClose` | `() => void` | no | noop | Reports completed closure once. |
32
+ | `onKeyDown` | `(event: SheetKeyboardEvent) => void` | no | noop | Observes keyboard input before default Escape handling. |
33
+
34
+ The default slot supplies dialog content. Include a native close button that
35
+ calls `useSheetContext().close()`.
36
+
37
+ ## Mediator state
38
+
39
+ | Property | Type | Description |
40
+ |---|---|---|
41
+ | `ariaLabel` | `string \| undefined` | Normalized dialog name. Empty labels are omitted. |
42
+ | `id` | `string` | Dialog ID from an explicit `id` or a generated `sheetController` prefix. |
43
+ | `isClosed` | `boolean` | Is `true` after closure is finalized and the surface should unmount. |
44
+ | `isClosing` | `boolean` | Is `true` during the exit animation; content is inert. |
45
+ | `isOpen` | `boolean` | Is `true` while the sheet is shown and not closing. |
46
+
47
+ ## CSS classes
48
+
49
+ | Class | Applied when | Notes |
50
+ |---|---|---|
51
+ | `SheetController` | Always | Root overlay class. |
52
+ | `has-backdrop` | `backdrop` is `true` | Backdrop overlay is present. |
53
+ | `is-open` | `isOpen` is `true` | Sheet is visible. |
54
+ | `is-modal` | `modal` is `true` | Modal focus containment is active. |
55
+ | `is-closing` | `isClosing` is `true` | Exit animation; pointer events disabled. |
56
+
57
+ ## CSS Variables
58
+
59
+ | Variable | Part | State | CSS property | Default | Description | Platforms |
60
+ |---|---|---|---|---|---|---|
61
+ | `--pw_SheetController_root_default_width` | `root` | `default` | `width` | `100%` | Overlay width. | Web, Capacitor, NativeScript |
62
+ | `--pw_SheetController_root_default_height` | `root` | `default` | `height` | `100%` | Overlay height. | Web, Capacitor, NativeScript |
63
+ | `--pw_SheetController_backdrop_default_background-color` | `backdrop` | `default` | `background-color` | shared backdrop | Backdrop colour. | Web, Capacitor, NativeScript |
64
+ | `--pw_SheetController_sheet_default_background-color` | `sheet` | `default` | `background-color` | shared background | Sheet background. | Web, Capacitor, NativeScript |
65
+ | `--pw_SheetController_sheet_default_color` | `sheet` | `default` | `color` | shared text | Sheet foreground. | Web, Capacitor, NativeScript |
66
+ | `--pw_SheetController_sheet_default_box-shadow` | `sheet` | `default` | `box-shadow` | elevation shadow | Sheet elevation. | Web, Capacitor |
67
+ | `--pw_SheetController_sheet_default_animation-duration` | `sheet` | `default` | `animation-duration` | `400ms` | Edge animation duration. | Web, Capacitor; native adapter |
68
+ | `--pw_SheetController_sheet_default_animation-timing-function` | `sheet` | `default` | `animation-timing-function` | `ease` | Edge animation timing. | Web, Capacitor; native adapter |
69
+ | `--pw_SheetController_sheet_default_animation-fill-mode` | `sheet` | `default` | `animation-fill-mode` | `forwards` | Edge animation fill. | Web, Capacitor; native adapter |
70
+ | `--pw_SheetController_sheet_default_outline` | `sheet` | `default` | `outline` | shared outline | Keyboard focus indicator. | Web, Capacitor |
71
+ | `--pw_SheetController_sheet_default_outline-offset` | `sheet` | `default` | `outline-offset` | `0.125em` | Focus outline offset. | Web, Capacitor |
72
+
73
+ ```xts
74
+ import { setContext, useBeforeUnmount, useMediator, useMounted, useRef, withDefaultProps } from '@pastweb/x';
75
+ import { sheetControllerMediator, SHEET_CONTEXT_KEY } from '@pastweb/components-core/SheetController';
76
+ import { DEFAULT_PROPS } from './constants';
77
+
78
+ export function SheetController(props: SheetControllerProps) {
79
+ const p = withDefaultProps<SheetControllerDefaultProps>(props, DEFAULT_PROPS);
80
+ const m = useMediator<SheetControllerMediator>(sheetControllerMediator, p);
81
+ const s = m.state;
82
+ const c = m.getContext();
83
+ const sheet = useRef();
84
+
85
+ setContext(SHEET_CONTEXT_KEY, c);
86
+ useMounted(() => m.useMounted(sheet.value, p.portalElement));
87
+ useBeforeUnmount(m.useBeforeUnmount);
88
+
89
+ if (s.isClosed) return null;
90
+
91
+ return (
92
+ <div class={p.cls('SheetController', {
93
+ 'has-backdrop': p.backdrop, 'is-closing': s.isClosing,
94
+ 'is-modal': p.modal, 'is-open': s.isOpen,
95
+ }, p.className)} #style={{ animation-duration: `${p.animationTime}ms` }}>
96
+ <button #if={p.backdrop} type="button" class={p.cls('backdrop')}
97
+ tabindex={-1} aria-hidden="true" @click={m.onBackdrop} />
98
+ <div #ref={sheet} id={m.id} class={p.cls('sheet', {
99
+ [`sheet-open-${p.direction}`]: s.isOpen,
100
+ [`sheet-close-${p.direction}`]: s.isClosing,
101
+ })} role="dialog" aria-label={s.ariaLabel} aria-modal={p.modal || undefined}
102
+ aria-hidden={s.isClosing || undefined} inert={s.isClosing || undefined} tabindex={-1}
103
+ @keydown={m.onKeyDown} @animationend={m.onTransitionEnd}>
104
+ <slot />
105
+ </div>
106
+ </div>
107
+ );
108
+ }
109
+ ```
110
+
111
+ ## Keyboard interaction
112
+
113
+ - Focus moves to the sheet when mounted.
114
+ - `Escape` closes when `closeOnEscape` is enabled, unless the consumer prevents the event.
115
+ - Modal sheets keep `Tab` and `Shift+Tab` inside their focusable content.
116
+ - Focus returns to the previously focused element when closing or unmounting.
117
+
118
+ ## Accessibility notes
119
+
120
+ - The web renderer exposes a named `dialog`. `aria-modal` is set only when `modal` is true.
121
+ - The backdrop is a native `button` excluded from the tab order and accessibility tree.
122
+ - Closing content becomes inert immediately; animation completion or the timer finalizes portal removal.
123
+ - Shared web styles keep a visible `:focus-visible` indicator and disable non-essential motion under `prefers-reduced-motion`.
124
+
125
+ ## Known limitations
126
+
127
+ - Nested sheets require distinct labels so dialog announcements remain understandable.
128
+ - A modal sheet still depends on its content to provide a visible close control.
129
+ - Automated accessibility checks do not by themselves establish WCAG conformance.
130
+
131
+ ## Native renderer mapping
132
+
133
+ NativeScript maps the sheet to an accessible modal or non-modal native container,
134
+ uses native focus containment and restoration, maps edge directions to native
135
+ animations, makes closing content unavailable immediately, and calls
136
+ `onTransitionEnd` when animation completes. Back gestures and Escape-compatible
137
+ keys call the same close intent.
138
+
139
+ ## Standards
140
+
141
+ - WCAG 2.2: https://www.w3.org/TR/WCAG22/
142
+ - WAI-ARIA Dialog (Modal) pattern: https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
143
+ - GOV.UK components: https://design-system.service.gov.uk/components/