mtrl 0.10.0-next.1 → 0.10.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +30 -5
  2. package/dist/README.md +30 -5
  3. package/dist/components/datepicker/constants.d.ts +1 -0
  4. package/dist/components/datepicker/constants.js +1 -0
  5. package/dist/components/datepicker/datepicker.js +70 -6
  6. package/dist/components/datepicker/render.js +43 -10
  7. package/dist/components/datepicker/types.d.ts +4 -1
  8. package/dist/components/datepicker/utils.d.ts +0 -7
  9. package/dist/components/datepicker/utils.js +0 -9
  10. package/dist/components/extended-fab/config.js +1 -1
  11. package/dist/components/extended-fab/types.d.ts +3 -2
  12. package/dist/components/segmented-button/segment.js +1 -1
  13. package/dist/components/slider/features/controller.js +6 -2
  14. package/dist/components/slider/features/states.d.ts +0 -3
  15. package/dist/components/slider/features/states.js +2 -15
  16. package/dist/components/textfield/constants.d.ts +11 -1
  17. package/dist/components/textfield/constants.js +5 -0
  18. package/dist/components/textfield/features/placement.js +45 -82
  19. package/dist/components/timepicker/api.js +47 -40
  20. package/dist/components/timepicker/config.js +0 -5
  21. package/dist/components/timepicker/dial.d.ts +30 -0
  22. package/dist/components/timepicker/dial.js +169 -0
  23. package/dist/components/timepicker/render.js +186 -275
  24. package/dist/components/timepicker/timepicker.d.ts +0 -5
  25. package/dist/components/timepicker/timepicker.js +5 -19
  26. package/dist/components/timepicker/types.d.ts +4 -1
  27. package/dist/elements/badge.d.ts +62 -0
  28. package/dist/elements/badge.js +33 -0
  29. package/dist/elements/button.d.ts +5 -8
  30. package/dist/elements/button.js +4 -19
  31. package/dist/elements/checkbox.d.ts +92 -0
  32. package/dist/elements/checkbox.js +49 -0
  33. package/dist/elements/css/checkbox.d.ts +1 -0
  34. package/dist/elements/css/checkbox.js +3 -0
  35. package/dist/elements/css/divider.d.ts +1 -0
  36. package/dist/elements/css/divider.js +3 -0
  37. package/dist/elements/css/extended-fab.d.ts +1 -0
  38. package/dist/elements/css/extended-fab.js +3 -0
  39. package/dist/elements/css/fab.d.ts +1 -0
  40. package/dist/elements/css/fab.js +3 -0
  41. package/dist/elements/css/icon-button.d.ts +1 -0
  42. package/dist/elements/css/icon-button.js +3 -0
  43. package/dist/elements/css/index.js +9 -0
  44. package/dist/elements/css/loading-indicator.d.ts +1 -0
  45. package/dist/elements/css/loading-indicator.js +3 -0
  46. package/dist/elements/css/radios.d.ts +1 -0
  47. package/dist/elements/css/radios.js +3 -0
  48. package/dist/elements/css/slider.d.ts +1 -0
  49. package/dist/elements/css/slider.js +3 -0
  50. package/dist/elements/css/switch.js +1 -1
  51. package/dist/elements/css/textfield.d.ts +1 -0
  52. package/dist/elements/css/textfield.js +3 -0
  53. package/dist/elements/define.d.ts +31 -6
  54. package/dist/elements/define.js +63 -11
  55. package/dist/elements/divider.d.ts +54 -0
  56. package/dist/elements/divider.js +23 -0
  57. package/dist/elements/extended-fab.d.ts +82 -0
  58. package/dist/elements/extended-fab.js +32 -0
  59. package/dist/elements/fab.d.ts +65 -0
  60. package/dist/elements/fab.js +24 -0
  61. package/dist/elements/form-button.d.ts +29 -0
  62. package/dist/elements/form-button.js +19 -0
  63. package/dist/elements/icon-button.d.ts +104 -0
  64. package/dist/elements/icon-button.js +56 -0
  65. package/dist/elements/index.d.ts +51 -1
  66. package/dist/elements/index.js +45 -0
  67. package/dist/elements/loading-indicator.d.ts +44 -0
  68. package/dist/elements/loading-indicator.js +26 -0
  69. package/dist/elements/progress.d.ts +90 -0
  70. package/dist/elements/progress.js +40 -0
  71. package/dist/elements/radios.d.ts +99 -0
  72. package/dist/elements/radios.js +126 -0
  73. package/dist/elements/slider.d.ts +160 -0
  74. package/dist/elements/slider.js +136 -0
  75. package/dist/elements/switch.d.ts +2 -1
  76. package/dist/elements/tabs.d.ts +3 -2
  77. package/dist/elements/tabs.js +3 -5
  78. package/dist/elements/textfield.d.ts +160 -0
  79. package/dist/elements/textfield.js +107 -0
  80. package/dist/index.cjs +0 -0
  81. package/dist/package.json +1 -1
  82. package/dist/react/index.d.ts +22 -1
  83. package/dist/react/index.js +22 -1
  84. package/dist/solid/index.d.ts +22 -1
  85. package/dist/solid/index.js +22 -1
  86. package/dist/styles/badge.css +1 -1
  87. package/dist/styles/base.css +1 -1
  88. package/dist/styles/bottom-app-bar.css +1 -1
  89. package/dist/styles/bottom-sheet.css +1 -1
  90. package/dist/styles/button-group.css +1 -1
  91. package/dist/styles/button.css +1 -1
  92. package/dist/styles/card.css +1 -1
  93. package/dist/styles/carousel.css +1 -1
  94. package/dist/styles/checkbox.css +1 -1
  95. package/dist/styles/chips.css +1 -1
  96. package/dist/styles/datepicker.css +2 -2
  97. package/dist/styles/dialog.css +1 -1
  98. package/dist/styles/divider.css +1 -1
  99. package/dist/styles/drawer.css +1 -1
  100. package/dist/styles/extended-fab.css +1 -1
  101. package/dist/styles/fab.css +1 -1
  102. package/dist/styles/icon-button.css +1 -1
  103. package/dist/styles/list.css +1 -1
  104. package/dist/styles/loading-indicator.css +1 -1
  105. package/dist/styles/menu.css +1 -1
  106. package/dist/styles/navigation-rail.css +1 -1
  107. package/dist/styles/progress.css +1 -1
  108. package/dist/styles/radios.css +2 -2
  109. package/dist/styles/search.css +1 -1
  110. package/dist/styles/segmented-button.css +1 -1
  111. package/dist/styles/select.css +1 -1
  112. package/dist/styles/side-sheet.css +1 -1
  113. package/dist/styles/slider.css +1 -1
  114. package/dist/styles/snackbar.css +1 -1
  115. package/dist/styles/split-button.css +1 -1
  116. package/dist/styles/switch.css +2 -2
  117. package/dist/styles/tabs.css +1 -1
  118. package/dist/styles/textfield.css +2 -2
  119. package/dist/styles/timepicker.css +2 -2
  120. package/dist/styles/tooltip.css +1 -1
  121. package/dist/styles/top-app-bar.css +1 -1
  122. package/dist/styles/utilities.css +1 -1
  123. package/dist/styles.css +2 -2
  124. package/dist/svelte/Badge.svelte +20 -0
  125. package/dist/svelte/Badge.svelte.d.ts +5 -0
  126. package/dist/svelte/Checkbox.svelte +21 -0
  127. package/dist/svelte/Checkbox.svelte.d.ts +5 -0
  128. package/dist/svelte/Divider.svelte +19 -0
  129. package/dist/svelte/Divider.svelte.d.ts +5 -0
  130. package/dist/svelte/ExtendedFab.svelte +19 -0
  131. package/dist/svelte/ExtendedFab.svelte.d.ts +5 -0
  132. package/dist/svelte/Fab.svelte +19 -0
  133. package/dist/svelte/Fab.svelte.d.ts +5 -0
  134. package/dist/svelte/IconButton.svelte +20 -0
  135. package/dist/svelte/IconButton.svelte.d.ts +5 -0
  136. package/dist/svelte/LoadingIndicator.svelte +19 -0
  137. package/dist/svelte/LoadingIndicator.svelte.d.ts +5 -0
  138. package/dist/svelte/Progress.svelte +21 -0
  139. package/dist/svelte/Progress.svelte.d.ts +5 -0
  140. package/dist/svelte/Radio.svelte +11 -0
  141. package/dist/svelte/Radio.svelte.d.ts +5 -0
  142. package/dist/svelte/Radios.svelte +20 -0
  143. package/dist/svelte/Radios.svelte.d.ts +5 -0
  144. package/dist/svelte/Slider.svelte +21 -0
  145. package/dist/svelte/Slider.svelte.d.ts +5 -0
  146. package/dist/svelte/Textfield.svelte +20 -0
  147. package/dist/svelte/Textfield.svelte.d.ts +5 -0
  148. package/dist/svelte/index.d.ts +12 -0
  149. package/dist/svelte/index.js +21 -0
  150. package/dist/themes/autumn.css +1 -1
  151. package/dist/themes/baseline.css +1 -1
  152. package/dist/themes/brownbeige.css +1 -1
  153. package/dist/themes/browngreen.css +1 -1
  154. package/dist/themes/desert.css +1 -1
  155. package/dist/themes/forest.css +1 -1
  156. package/dist/themes/highcontrast.css +1 -1
  157. package/dist/themes/legacy.css +1 -1
  158. package/dist/themes/material.css +1 -1
  159. package/dist/themes/ocean.css +1 -1
  160. package/dist/themes/sageivory.css +1 -1
  161. package/dist/themes/spring.css +1 -1
  162. package/dist/themes/summer.css +1 -1
  163. package/dist/themes/sunset.css +1 -1
  164. package/dist/themes/tealcaramel.css +1 -1
  165. package/dist/themes/winter.css +1 -1
  166. package/dist/vue/index.d.ts +22 -1
  167. package/dist/vue/index.js +22 -1
  168. package/package.json +1 -1
  169. package/src/styles/components/_datepicker.scss +52 -14
  170. package/src/styles/components/_radios.scss +2 -3
  171. package/src/styles/components/_switch.scss +2 -2
  172. package/src/styles/components/_textfield.scss +122 -70
  173. package/src/styles/components/_timepicker.scss +156 -67
  174. package/dist/components/timepicker/clockdial.d.ts +0 -34
  175. package/dist/components/timepicker/clockdial.js +0 -276
package/README.md CHANGED
@@ -136,23 +136,48 @@ Button progress and card actions load on demand; enable code splitting in your b
136
136
 
137
137
  ## Web components and frameworks (experimental)
138
138
 
139
- From `0.10.0-next.1` (`npm install mtrl@next`), components also ship as custom elements, with thin adapters for React, Vue, Svelte and Solid. Button, switch and tabs are available so far; more follow through 0.10.x. The API may still change.
139
+ From the 0.10.0 prereleases (`npm install mtrl@next`), components also ship as custom elements, with thin adapters for React, Vue, Svelte and Solid. The API may still change.
140
+
141
+ | Element | Framework component (Vue: `M` prefix, e.g. `MTextfield`) | Kind |
142
+ |---------|---------------------|------|
143
+ | `<m-button>` | `Button` | action, submits or resets its form |
144
+ | `<m-icon-button>` | `IconButton` | action or toggle (`selected`) |
145
+ | `<m-fab>`, `<m-extended-fab>` | `Fab`, `ExtendedFab` | action |
146
+ | `<m-checkbox>` | `Checkbox` | form control (`checked`, `indeterminate`) |
147
+ | `<m-switch>` | `Switch` | form control (`checked`) |
148
+ | `<m-radios>` with `<m-radio>` | `Radios`, `Radio` | form control (`value`) |
149
+ | `<m-slider>` | `Slider` | form control (`value`, `secondValue` with `range`) |
150
+ | `<m-textfield>` | `Textfield` | form control (`value`) |
151
+ | `<m-tabs>` with `<m-tab>` | `Tabs`, `Tab` | selection (`value`) |
152
+ | `<m-progress>`, `<m-loading-indicator>` | `Progress`, `LoadingIndicator` | indicator |
153
+ | `<m-badge>`, `<m-divider>` | `Badge`, `Divider` | display |
154
+
155
+ More components follow through 0.10.x; the others are available as factories today.
140
156
 
141
157
  ```html
142
158
  <script type="module">
143
159
  import 'mtrl/elements/css'; // the elements' styles
144
160
  import { defineAll } from 'mtrl/elements';
145
- defineAll(); // registers <m-button>, <m-switch>, <m-tabs>, <m-tab>
161
+ defineAll(); // registers every element (<m-button>, <m-switch>, …)
146
162
  </script>
147
163
 
148
- <m-switch name="wifi" checked>Wi-Fi</m-switch>
164
+ <form>
165
+ <m-textfield name="email" type="email" label="Email" required></m-textfield>
166
+ <m-radios name="plan" value="free" aria-label="Plan">
167
+ <m-radio value="free">Free</m-radio>
168
+ <m-radio value="pro">Pro</m-radio>
169
+ </m-radios>
170
+ <m-switch name="news" checked>Newsletter</m-switch>
171
+ <m-button type="submit">Sign up</m-button>
172
+ </form>
173
+
149
174
  <m-tabs value="songs">
150
175
  <m-tab value="songs">Songs</m-tab>
151
176
  <m-tab value="albums">Albums</m-tab>
152
177
  </m-tabs>
153
178
  ```
154
179
 
155
- The page still loads mtrl's base styles and a theme (see [Styles](#styles)); the elements render in shadow DOM and pick up the theme's tokens. Attributes are defaults and properties the live state, as on native controls; form controls take part in forms, reset and validation.
180
+ The page still loads mtrl's base styles and a theme (see [Styles](#styles)); the elements render in shadow DOM and pick up the theme's tokens. Attributes are defaults and properties the live state, as on native controls: a `checked` or `value` attribute sets the state until the user or a script changes it, and `form.reset()` goes back to it. Form controls take part in forms (their host's `name`), reset, validation and back-navigation restore.
156
181
 
157
182
  | Framework | Import | Two-way binding |
158
183
  |-----------|--------|-----------------|
@@ -161,7 +186,7 @@ The page still loads mtrl's base styles and a theme (see [Styles](#styles)); the
161
186
  | Svelte 5 | `import { Switch } from 'mtrl/svelte'` | `bind:checked` |
162
187
  | Solid | `import { Switch } from 'mtrl/solid'` | `checked` + `onChange` |
163
188
 
164
- Each framework is an optional peer dependency; mtrl installs none of them. All adapters render on the server and hydrate.
189
+ Each framework is an optional peer dependency; mtrl installs none of them. All adapters render on the server and hydrate. Angular apps use the elements directly, with `CUSTOM_ELEMENTS_SCHEMA`.
165
190
 
166
191
  ## Building your own components
167
192
 
package/dist/README.md CHANGED
@@ -136,23 +136,48 @@ Button progress and card actions load on demand; enable code splitting in your b
136
136
 
137
137
  ## Web components and frameworks (experimental)
138
138
 
139
- From `0.10.0-next.1` (`npm install mtrl@next`), components also ship as custom elements, with thin adapters for React, Vue, Svelte and Solid. Button, switch and tabs are available so far; more follow through 0.10.x. The API may still change.
139
+ From the 0.10.0 prereleases (`npm install mtrl@next`), components also ship as custom elements, with thin adapters for React, Vue, Svelte and Solid. The API may still change.
140
+
141
+ | Element | Framework component (Vue: `M` prefix, e.g. `MTextfield`) | Kind |
142
+ |---------|---------------------|------|
143
+ | `<m-button>` | `Button` | action, submits or resets its form |
144
+ | `<m-icon-button>` | `IconButton` | action or toggle (`selected`) |
145
+ | `<m-fab>`, `<m-extended-fab>` | `Fab`, `ExtendedFab` | action |
146
+ | `<m-checkbox>` | `Checkbox` | form control (`checked`, `indeterminate`) |
147
+ | `<m-switch>` | `Switch` | form control (`checked`) |
148
+ | `<m-radios>` with `<m-radio>` | `Radios`, `Radio` | form control (`value`) |
149
+ | `<m-slider>` | `Slider` | form control (`value`, `secondValue` with `range`) |
150
+ | `<m-textfield>` | `Textfield` | form control (`value`) |
151
+ | `<m-tabs>` with `<m-tab>` | `Tabs`, `Tab` | selection (`value`) |
152
+ | `<m-progress>`, `<m-loading-indicator>` | `Progress`, `LoadingIndicator` | indicator |
153
+ | `<m-badge>`, `<m-divider>` | `Badge`, `Divider` | display |
154
+
155
+ More components follow through 0.10.x; the others are available as factories today.
140
156
 
141
157
  ```html
142
158
  <script type="module">
143
159
  import 'mtrl/elements/css'; // the elements' styles
144
160
  import { defineAll } from 'mtrl/elements';
145
- defineAll(); // registers <m-button>, <m-switch>, <m-tabs>, <m-tab>
161
+ defineAll(); // registers every element (<m-button>, <m-switch>, …)
146
162
  </script>
147
163
 
148
- <m-switch name="wifi" checked>Wi-Fi</m-switch>
164
+ <form>
165
+ <m-textfield name="email" type="email" label="Email" required></m-textfield>
166
+ <m-radios name="plan" value="free" aria-label="Plan">
167
+ <m-radio value="free">Free</m-radio>
168
+ <m-radio value="pro">Pro</m-radio>
169
+ </m-radios>
170
+ <m-switch name="news" checked>Newsletter</m-switch>
171
+ <m-button type="submit">Sign up</m-button>
172
+ </form>
173
+
149
174
  <m-tabs value="songs">
150
175
  <m-tab value="songs">Songs</m-tab>
151
176
  <m-tab value="albums">Albums</m-tab>
152
177
  </m-tabs>
153
178
  ```
154
179
 
155
- The page still loads mtrl's base styles and a theme (see [Styles](#styles)); the elements render in shadow DOM and pick up the theme's tokens. Attributes are defaults and properties the live state, as on native controls; form controls take part in forms, reset and validation.
180
+ The page still loads mtrl's base styles and a theme (see [Styles](#styles)); the elements render in shadow DOM and pick up the theme's tokens. Attributes are defaults and properties the live state, as on native controls: a `checked` or `value` attribute sets the state until the user or a script changes it, and `form.reset()` goes back to it. Form controls take part in forms (their host's `name`), reset, validation and back-navigation restore.
156
181
 
157
182
  | Framework | Import | Two-way binding |
158
183
  |-----------|--------|-----------------|
@@ -161,7 +186,7 @@ The page still loads mtrl's base styles and a theme (see [Styles](#styles)); the
161
186
  | Svelte 5 | `import { Switch } from 'mtrl/svelte'` | `bind:checked` |
162
187
  | Solid | `import { Switch } from 'mtrl/solid'` | `checked` + `onChange` |
163
188
 
164
- Each framework is an optional peer dependency; mtrl installs none of them. All adapters render on the server and hydrate.
189
+ Each framework is an optional peer dependency; mtrl installs none of them. All adapters render on the server and hydrate. Angular apps use the elements directly, with `CUSTOM_ELEMENTS_SCHEMA`.
165
190
 
166
191
  ## Building your own components
167
192
 
@@ -101,4 +101,5 @@ export declare const DATEPICKER_ICONS: {
101
101
  readonly edit: "<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"m3 17.25 11.06-11.06 3.75 3.75L6.75 21H3v-3.75ZM20.71 7.04l-1.83 1.83-3.75-3.75 1.83-1.83a1 1 0 0 1 1.41 0l2.34 2.34a1 1 0 0 1 0 1.41Z\"/></svg>";
102
102
  readonly previous: "<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"m15.4 7.4-1.4-1.4-6 6 6 6 1.4-1.4-4.6-4.6Z\"/></svg>";
103
103
  readonly next: "<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"m8.6 7.4 1.4-1.4 6 6-6 6-1.4-1.4 4.6-4.6Z\"/></svg>";
104
+ readonly close: "<svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" fill=\"currentColor\" aria-hidden=\"true\"><path d=\"M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12Z\"/></svg>";
104
105
  };
@@ -53,4 +53,5 @@ export const DATEPICKER_ICONS = {
53
53
  edit: '<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true"><path d="m3 17.25 11.06-11.06 3.75 3.75L6.75 21H3v-3.75ZM20.71 7.04l-1.83 1.83-3.75-3.75 1.83-1.83a1 1 0 0 1 1.41 0l2.34 2.34a1 1 0 0 1 0 1.41Z"/></svg>',
54
54
  previous: '<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true"><path d="m15.4 7.4-1.4-1.4-6 6 6 6 1.4-1.4-4.6-4.6Z"/></svg>',
55
55
  next: '<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true"><path d="m8.6 7.4 1.4-1.4 6 6-6 6-1.4-1.4 4.6-4.6Z"/></svg>',
56
+ close: '<svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12Z"/></svg>',
56
57
  };
@@ -33,10 +33,10 @@ const createDatePicker = (config = {}) => {
33
33
  const today = parseDate(new Date());
34
34
  const state = {
35
35
  id, prefix: PREFIX, label: settings.label || "Select date", selectedDate: null, rangeEndDate: null,
36
- currentView: ["month", "year"].includes(settings.initialView ?? "") ? settings.initialView : "day",
36
+ currentView: settings.variant !== "fullscreen" && ["month", "year"].includes(settings.initialView ?? "") ? settings.initialView : "day",
37
37
  currentMonth: today.getMonth(), currentYear: today.getFullYear(), focusedDate: today,
38
38
  minDate: parseDate(settings.minDate ?? null), maxDate: parseDate(settings.maxDate ?? null),
39
- dateFormat: settings.dateFormat || "MM/DD/YYYY", variant: settings.variant === "modal" || settings.variant === "modal-input" ? settings.variant : "docked",
39
+ dateFormat: settings.dateFormat || "MM/DD/YYYY", variant: settings.variant === "modal" || settings.variant === "modal-input" || settings.variant === "fullscreen" ? settings.variant : "docked",
40
40
  selectionMode: settings.selectionMode === "range" ? "range" : "single", inputMode: settings.variant === "modal-input",
41
41
  specialDates: (settings.specialDates ?? []).map(item => ({ ...item })),
42
42
  isAllowed: date => (!state.minDate || date >= state.minDate) && (!state.maxDate || date <= state.maxDate) && !state.specialDates.some(item => { const special = parseDate(item.date); return item.disabled && special && isSameDay(date, special); }),
@@ -73,6 +73,8 @@ const createDatePicker = (config = {}) => {
73
73
  dialog.id = `${id}-dialog`;
74
74
  dialog.className = cls("calendar");
75
75
  dialog.classList.add(base.getClass(`datepicker--${modal ? "modal" : "docked"}`));
76
+ if (state.variant === "fullscreen")
77
+ dialog.classList.add(base.getClass("datepicker--fullscreen"));
76
78
  dialog.setAttribute("aria-modal", String(modal));
77
79
  if (settings.animate)
78
80
  dialog.classList.add(base.getClass("datepicker--animate"));
@@ -105,11 +107,65 @@ const createDatePicker = (config = {}) => {
105
107
  const element = track();
106
108
  if (element?.clientWidth)
107
109
  element.scrollLeft = (rtl(element) ? -1 : 1) * element.clientWidth;
110
+ const years = dialog.querySelector(`.${cls("years")}`);
111
+ const year = years?.querySelector('[aria-pressed="true"]');
112
+ if (years && year)
113
+ years.scrollTop = year.offsetTop - (years.clientHeight - year.offsetHeight) / 2;
114
+ };
115
+ const monthIndex = (date) => date.getFullYear() * 12 + date.getMonth();
116
+ const fromIndex = (index) => new Date(Math.floor(index / 12), index % 12, 1);
117
+ const list = () => dialog.querySelector(`.${cls("list")}`);
118
+ const limits = () => [state.minDate ? monthIndex(state.minDate) : -Infinity, state.maxDate ? monthIndex(state.maxDate) : Infinity];
119
+ let shiftBy = null;
120
+ const ensureWindow = () => {
121
+ if (state.variant !== "fullscreen")
122
+ return;
123
+ const focus = state.currentYear * 12 + state.currentMonth;
124
+ const start = state.listStart ? monthIndex(state.listStart) : NaN;
125
+ if (start <= focus && focus < start + (state.listLength ?? 0))
126
+ return;
127
+ const [min, max] = limits();
128
+ const first = Math.max(min, focus - 12), last = Math.min(max, focus + 12);
129
+ state.listStart = fromIndex(first);
130
+ state.listLength = last - first + 1;
131
+ };
132
+ const extend = (element) => {
133
+ const [min, max] = limits();
134
+ const start = monthIndex(state.listStart), end = start + state.listLength - 1;
135
+ const margin = element.clientHeight;
136
+ if (element.scrollTop < margin && start > min) {
137
+ const first = Math.max(min, start - 12);
138
+ state.listStart = fromIndex(first);
139
+ state.listLength = end - first + 1;
140
+ shiftBy = element.scrollHeight;
141
+ }
142
+ else if (element.scrollHeight - element.scrollTop - element.clientHeight < margin && end < max) {
143
+ state.listLength = Math.min(max, end + 12) - start + 1;
144
+ }
145
+ else
146
+ return;
147
+ const active = doc.activeElement instanceof HTMLElement ? doc.activeElement.dataset.date : undefined;
148
+ render();
149
+ if (active)
150
+ dialog.querySelector(`[data-date="${active}"]`)?.focus({ preventScroll: true });
151
+ };
152
+ const reveal = () => {
153
+ const element = list();
154
+ const section = element?.querySelector(`[data-month-key="${state.currentYear}-${state.currentMonth}"]`);
155
+ if (element && section)
156
+ element.scrollTop = section.offsetTop;
108
157
  };
109
158
  const render = (focus = false, action) => {
110
159
  if (!opened || destroyed)
111
160
  return;
161
+ ensureWindow();
162
+ const previous = list();
163
+ const kept = previous && { top: previous.scrollTop, height: previous.scrollHeight };
112
164
  dialog.replaceChildren(renderCalendar(state), announcement);
165
+ const current = list();
166
+ if (current && kept)
167
+ current.scrollTop = kept.top + (shiftBy === null ? 0 : current.scrollHeight - kept.height);
168
+ shiftBy = null;
113
169
  centre();
114
170
  announcement.textContent = new Date(state.currentYear, state.currentMonth, 1).toLocaleDateString("en-US", { month: "long", year: "numeric" });
115
171
  if (action)
@@ -150,6 +206,7 @@ const createDatePicker = (config = {}) => {
150
206
  opened = true;
151
207
  resetDraft();
152
208
  state.inputMode = state.variant === "modal-input";
209
+ state.listStart = undefined;
153
210
  let initial = committed ?? today;
154
211
  if (state.minDate && initial < state.minDate)
155
212
  initial = state.minDate;
@@ -169,6 +226,7 @@ const createDatePicker = (config = {}) => {
169
226
  if (modal)
170
227
  unlock = lockScroll(doc);
171
228
  centre();
229
+ reveal();
172
230
  trigger.setAttribute("aria-expanded", "true");
173
231
  focusCurrent();
174
232
  base.emit("open", { value: getValue() });
@@ -256,7 +314,7 @@ const createDatePicker = (config = {}) => {
256
314
  render(true);
257
315
  };
258
316
  const navigate = (amount) => {
259
- const count = state.currentView === "day" ? amount : state.currentView === "month" ? amount * 12 : amount * 12 * 21;
317
+ const count = state.currentView === "day" ? amount : amount * 12;
260
318
  const anchor = new Date(state.currentYear, state.currentMonth, 1);
261
319
  anchor.setDate(Math.min(state.focusedDate.getDate(), new Date(state.currentYear, state.currentMonth + 1, 0).getDate()));
262
320
  setDisplayDate(addMonths(anchor, count));
@@ -363,10 +421,11 @@ const createDatePicker = (config = {}) => {
363
421
  if (target?.dataset.date) {
364
422
  const date = parseDate(target.dataset.date);
365
423
  let next;
424
+ const ahead = rtl(dialog) ? -1 : 1;
366
425
  if (event.key === "ArrowLeft")
367
- next = addDays(date, -1);
426
+ next = addDays(date, -ahead);
368
427
  else if (event.key === "ArrowRight")
369
- next = addDays(date, 1);
428
+ next = addDays(date, ahead);
370
429
  else if (event.key === "ArrowUp")
371
430
  next = addDays(date, -7);
372
431
  else if (event.key === "ArrowDown")
@@ -395,7 +454,8 @@ const createDatePicker = (config = {}) => {
395
454
  else if (target?.hasAttribute('data-month') || target?.hasAttribute('data-year')) {
396
455
  const buttons = Array.from(dialog.querySelectorAll('[data-month], [data-year]'));
397
456
  let index = buttons.indexOf(target);
398
- const delta = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -3, ArrowDown: 3 }[event.key];
457
+ const ahead = rtl(dialog) ? -1 : 1;
458
+ const delta = { ArrowLeft: -ahead, ArrowRight: ahead, ArrowUp: -3, ArrowDown: 3 }[event.key];
399
459
  if (event.key === "Home")
400
460
  index = 0;
401
461
  else if (event.key === "End")
@@ -430,6 +490,10 @@ const createDatePicker = (config = {}) => {
430
490
  render(!!active?.dataset.date, active?.dataset.action);
431
491
  };
432
492
  const onScroll = (event) => {
493
+ if (event.type === "scroll" && event.target instanceof HTMLElement && event.target === list()) {
494
+ extend(event.target);
495
+ return;
496
+ }
433
497
  if (!(event.target instanceof HTMLElement) || event.target !== track())
434
498
  return;
435
499
  if (event.type === "scrollend") {
@@ -2,20 +2,22 @@ import { DATEPICKER_ICONS } from "./constants.js";
2
2
  import { setHTML } from "../../core/dom/html.js";
3
3
  import { createElement } from "../../core/dom/create.js";
4
4
  import { MONTH_NAMES, MONTH_NAMES_SHORT } from "./types.js";
5
- import { formatDate, generateCalendarDates, generateYearRange, isSameDay, parseDate } from "./utils.js";
5
+ import { formatDate, generateCalendarDates, isSameDay, parseDate } from "./utils.js";
6
6
  export function renderCalendar(state) {
7
7
  const cls = (name) => `${state.prefix}-datepicker__${name}`;
8
8
  const make = (tag, name, text, attributes) => createElement({ tag, className: cls(name), text, attributes });
9
9
  const button = (name, text, action, label = text) => {
10
10
  const element = make("button", name, text, { type: "button", "data-action": action, "aria-label": label });
11
- const icon = action === "toggle-mode" ? (state.inputMode ? DATEPICKER_ICONS.calendar : DATEPICKER_ICONS.edit) : action === "prev" ? DATEPICKER_ICONS.previous : action === "next" ? DATEPICKER_ICONS.next : null;
11
+ const icon = name === "close" ? DATEPICKER_ICONS.close : action === "toggle-mode" ? (state.inputMode ? DATEPICKER_ICONS.calendar : DATEPICKER_ICONS.edit) : action === "prev" ? DATEPICKER_ICONS.previous : action === "next" ? DATEPICKER_ICONS.next : null;
12
12
  if (icon)
13
13
  setHTML(element, icon);
14
14
  return element;
15
15
  };
16
- const monthGrid = (year, month, current) => {
16
+ const monthGrid = (year, month, current, list = false) => {
17
17
  const grid = make("div", "days", undefined, { role: "grid", "aria-label": `${MONTH_NAMES[month]} ${year}`, "aria-describedby": `${state.id}-keyboard` });
18
18
  const weekdays = make("div", "weekdays", undefined, { role: "row" });
19
+ if (list)
20
+ weekdays.classList.add(cls("sr-only"));
19
21
  for (const name of ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"])
20
22
  weekdays.append(make("span", "weekday", name[0], { role: "columnheader", "aria-label": name }));
21
23
  grid.append(weekdays);
@@ -26,6 +28,10 @@ export function renderCalendar(state) {
26
28
  for (const item of dates.slice(week * 7, week * 7 + 7)) {
27
29
  const selected = !!state.selectedDate && (isSameDay(item.date, state.selectedDate) || !!state.rangeEndDate && isSameDay(item.date, state.rangeEndDate));
28
30
  const inRange = !!state.selectedDate && !!state.rangeEndDate && item.date >= state.selectedDate && item.date <= state.rangeEndDate;
31
+ if (list && !item.isCurrentMonth) {
32
+ row.append(make("div", "cell", undefined, { role: "gridcell" }));
33
+ continue;
34
+ }
29
35
  const cell = make("div", "cell", undefined, { role: "gridcell", "aria-selected": String(selected || inRange) });
30
36
  if (inRange)
31
37
  cell.classList.add(cls("cell--range"));
@@ -61,11 +67,19 @@ export function renderCalendar(state) {
61
67
  };
62
68
  const content = make("div", "content");
63
69
  const modal = state.variant !== "docked";
70
+ const fullscreen = state.variant === "fullscreen";
71
+ const confirmDisabled = !state.selectedDate || !state.isAllowed(state.selectedDate) || state.selectionMode === "range" && (!state.rangeEndDate || !state.isAllowed(state.rangeEndDate));
64
72
  if (modal) {
65
73
  const header = make("div", "modal-header");
74
+ if (fullscreen) {
75
+ const save = button("save", "Save", "confirm");
76
+ save.disabled = confirmDisabled;
77
+ header.append(button("close", "", "cancel", "Close"), save);
78
+ }
66
79
  header.append(make("div", "title", state.label, { id: `${state.id}-title` }));
67
- const headline = state.selectedDate ? formatDate(state.selectedDate, "MMM D, YYYY") : "Select date";
68
- header.append(make("div", "headline", state.selectionMode === "range" && state.rangeEndDate ? `${headline} – ${formatDate(state.rangeEndDate, "MMM D, YYYY")}` : headline));
80
+ const pattern = fullscreen ? "MMM D" : "MMM D, YYYY";
81
+ const headline = state.selectedDate ? formatDate(state.selectedDate, pattern) : state.selectionMode === "range" ? "Select range" : "Select date";
82
+ header.append(make("div", "headline", state.selectionMode === "range" && state.rangeEndDate ? `${headline} – ${formatDate(state.rangeEndDate, pattern)}` : headline));
69
83
  header.append(button("mode-toggle", state.inputMode ? "▦" : "✎", "toggle-mode", state.inputMode ? "Switch to calendar" : "Switch to date input"));
70
84
  content.append(header);
71
85
  }
@@ -82,12 +96,30 @@ export function renderCalendar(state) {
82
96
  }
83
97
  content.append(fields);
84
98
  }
99
+ else if (fullscreen) {
100
+ const weekdays = make("div", "weekdays", undefined, { "aria-hidden": "true" });
101
+ for (const name of ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"])
102
+ weekdays.append(make("span", "weekday", name[0]));
103
+ const list = make("div", "list");
104
+ const start = state.listStart ?? new Date(state.currentYear, state.currentMonth, 1);
105
+ for (let index = 0; index < (state.listLength ?? 1); index++) {
106
+ const first = new Date(start.getFullYear(), start.getMonth() + index, 1);
107
+ const section = make("section", "month-section", undefined, { "data-month-key": `${first.getFullYear()}-${first.getMonth()}` });
108
+ section.append(make("div", "subhead", `${MONTH_NAMES[first.getMonth()]} ${first.getFullYear()}`, { "aria-hidden": "true" }));
109
+ section.append(monthGrid(first.getFullYear(), first.getMonth(), first.getFullYear() === state.focusedDate.getFullYear() && first.getMonth() === state.focusedDate.getMonth(), true));
110
+ list.append(section);
111
+ }
112
+ content.append(weekdays, list, make("div", "sr-only", "Use arrow keys for days, Home and End for the week, Page Up and Page Down for months, with Shift for years.", { id: `${state.id}-keyboard` }));
113
+ }
85
114
  else {
86
115
  const header = make("div", "header");
87
116
  const nav = make("div", "navigation");
88
117
  nav.append(button("month-selector", MONTH_NAMES[state.currentMonth], "month", "Select month"), button("year-selector", String(state.currentYear), "year", "Select year"));
89
- const span = state.currentView === "day" ? "month" : state.currentView === "month" ? "year" : "year range";
90
- header.append(nav, button("prev", "‹", "prev", `Previous ${span}`), button("next", "›", "next", `Next ${span}`));
118
+ header.append(nav);
119
+ if (state.currentView !== "year") {
120
+ const span = state.currentView === "day" ? "month" : "year";
121
+ header.append(button("prev", "‹", "prev", `Previous ${span}`), button("next", "›", "next", `Next ${span}`));
122
+ }
91
123
  content.append(header);
92
124
  if (state.currentView === "day") {
93
125
  const track = make("div", "track");
@@ -106,7 +138,8 @@ export function renderCalendar(state) {
106
138
  else {
107
139
  const months = state.currentView === "month";
108
140
  const grid = make("div", months ? "months" : "years", undefined, { role: "group", "aria-label": months ? "Choose month" : "Choose year" });
109
- for (const value of months ? Array.from({ length: 12 }, (_, i) => i) : generateYearRange(state.currentYear)) {
141
+ const first = state.minDate?.getFullYear() ?? 1900, last = state.maxDate?.getFullYear() ?? 2100;
142
+ for (const value of months ? Array.from({ length: 12 }, (_, i) => i) : Array.from({ length: last - first + 1 }, (_, i) => first + i)) {
110
143
  const selected = value === (months ? state.currentMonth : state.currentYear);
111
144
  grid.append(make("button", months ? "month" : "year", months ? MONTH_NAMES_SHORT[value] : String(value), {
112
145
  type: "button", [months ? "data-month" : "data-year"]: String(value), "aria-pressed": String(selected), tabindex: selected ? "0" : "-1",
@@ -116,10 +149,10 @@ export function renderCalendar(state) {
116
149
  }
117
150
  content.append(make("div", "sr-only", "Use arrow keys for days, Home and End for the week, Page Up and Page Down for months, with Shift for years.", { id: `${state.id}-keyboard` }));
118
151
  }
119
- if (modal) {
152
+ if (modal && !fullscreen) {
120
153
  const footer = make("div", "footer");
121
154
  const confirm = button("confirm", "OK", "confirm");
122
- confirm.disabled = !state.selectedDate || !state.isAllowed(state.selectedDate) || state.selectionMode === "range" && (!state.rangeEndDate || !state.isAllowed(state.rangeEndDate));
155
+ confirm.disabled = confirmDisabled;
123
156
  footer.append(button("cancel", "Cancel", "cancel"), confirm);
124
157
  content.append(footer);
125
158
  }
@@ -41,7 +41,7 @@ export interface DatePickerEvents {
41
41
  * DatePicker variant types
42
42
  * @category Components
43
43
  */
44
- export type DatePickerVariant = 'docked' | 'modal' | 'modal-input';
44
+ export type DatePickerVariant = 'docked' | 'modal' | 'modal-input' | 'fullscreen';
45
45
  /**
46
46
  * DatePicker view types
47
47
  * @category Components
@@ -361,4 +361,7 @@ export interface DatePickerState {
361
361
  label: string;
362
362
  specialDates: NonNullable<DatePickerConfig["specialDates"]>;
363
363
  isAllowed(date: Date): boolean;
364
+ /** Full screen: the first month of the rendered list, and how many follow. FLO-276. */
365
+ listStart?: Date;
366
+ listLength?: number;
364
367
  }
@@ -56,13 +56,6 @@ export declare const isDateInRange: (date: Date, startDate: Date, endDate: Date)
56
56
  * @returns Array of calendar dates
57
57
  */
58
58
  export declare const generateCalendarDates: (year: number, month: number, selectedDate?: Date | null, rangeEndDate?: Date | null, minDate?: Date | null, maxDate?: Date | null) => CalendarDate[];
59
- /**
60
- * Generates an array of years for year selection
61
- * @param currentYear - Center year for the range
62
- * @param range - Number of years before and after current year
63
- * @returns Array of years
64
- */
65
- export declare const generateYearRange: (currentYear: number, range?: number) => number[];
66
59
  /**
67
60
  * Adds days to a date
68
61
  * @param date - Base date
@@ -162,15 +162,6 @@ export const generateCalendarDates = (year, month, selectedDate = null, rangeEnd
162
162
  }
163
163
  return result;
164
164
  };
165
- export const generateYearRange = (currentYear, range = 10) => {
166
- const years = [];
167
- const startYear = currentYear - range;
168
- const endYear = currentYear + range;
169
- for (let year = startYear; year <= endYear; year++) {
170
- years.push(year);
171
- }
172
- return years;
173
- };
174
165
  export const addDays = (date, days) => {
175
166
  const result = new Date(date);
176
167
  result.setDate(result.getDate() + days);
@@ -12,7 +12,7 @@ export const createBaseConfig = (config = {}) => createComponentConfig(defaultCo
12
12
  export const getElementConfig = (config) => {
13
13
  const attributes = {
14
14
  type: config.type || "button",
15
- "aria-label": config.ariaLabel || config.text || (config.icon ? "action" : undefined),
15
+ "aria-label": config.ariaLabel,
16
16
  };
17
17
  if (config.value !== undefined) {
18
18
  attributes.value = config.value;
@@ -237,8 +237,9 @@ export interface ExtendedFabConfig {
237
237
  /**
238
238
  * Accessible label for screen readers
239
239
  *
240
- * Sets the aria-label attribute for accessibility. Especially important
241
- * if the Extended FAB doesn't include text content.
240
+ * Sets the aria-label attribute. The visible text already names the
241
+ * Extended FAB, so set this only when the name should differ from it or
242
+ * there is no text. Nothing is derived from `text` or `icon` (#232).
242
243
  *
243
244
  * @example
244
245
  * ```typescript
@@ -30,7 +30,7 @@ export const createSegment = (config, container, prefix, groupDisabled = false,
30
30
  ripple: options.ripple,
31
31
  rippleConfig: options.rippleConfig,
32
32
  class: config.class,
33
- ariaLabel: config.text || config.value,
33
+ ariaLabel: isIconOnly ? config.value : undefined,
34
34
  });
35
35
  button.element.classList.add(`${prefix}-segmented-button__segment`);
36
36
  let checkmarkElement = null;
@@ -224,13 +224,17 @@ export const withController = (config, getComponent) => (component) => {
224
224
  console.error("Error initializing slider controller:", error);
225
225
  }
226
226
  };
227
- const initialization = setTimeout(initController, 0);
227
+ initController();
228
+ const layout = typeof ResizeObserver !== "undefined" ? new ResizeObserver(() => updateHandlePositions()) : null;
229
+ layout?.observe(component.element);
228
230
  if (component.lifecycle) {
229
231
  const originalDestroy = component.lifecycle.destroy || (() => { });
230
232
  component.lifecycle.destroy = () => {
231
- clearTimeout(initialization);
233
+ layout?.disconnect();
232
234
  if (settleTimer !== null)
233
235
  clearTimeout(settleTimer);
236
+ if (state.valueHideTimer !== null)
237
+ clearTimeout(state.valueHideTimer);
234
238
  handlers.cleanupEventListeners();
235
239
  originalDestroy.call(component.lifecycle);
236
240
  };
@@ -17,9 +17,6 @@ interface StatesHost {
17
17
  structure?: Record<string, HTMLElement | undefined>;
18
18
  formFields?: (HTMLInputElement | null)[] | null;
19
19
  renderTracks?: (state?: unknown) => void;
20
- lifecycle?: {
21
- destroy: () => void;
22
- };
23
20
  }
24
21
  export declare const withStates: (config: SliderConfig) => <C extends StatesHost>(component: C) => C & {
25
22
  disabled: {
@@ -3,13 +3,6 @@ import { SLIDER_SIZES } from "../constants.js";
3
3
  export const withStates = (config) => (component) => {
4
4
  const handle = component.handle ?? component.structure?.handle;
5
5
  const secondHandle = component.secondHandle ?? component.structure?.secondHandle;
6
- const isDisabled = config.disabled === true;
7
- let initialization = null;
8
- if (isDisabled) {
9
- initialization = setTimeout(() => {
10
- disableComponent();
11
- }, 0);
12
- }
13
6
  const setFieldsDisabled = (disabled) => {
14
7
  for (const field of component.formFields ?? []) {
15
8
  if (field)
@@ -56,14 +49,8 @@ export const withStates = (config) => (component) => {
56
49
  const foundKey = sizeKeys.find((sizeKey) => component.element.classList.contains(`${component.getClass("slider")}--${sizeKey.toLowerCase()}`));
57
50
  return foundKey || "XS";
58
51
  }
59
- if (component.lifecycle) {
60
- const originalDestroy = component.lifecycle.destroy;
61
- component.lifecycle.destroy = () => {
62
- if (initialization !== null)
63
- clearTimeout(initialization);
64
- originalDestroy.call(component.lifecycle);
65
- };
66
- }
52
+ if (config.disabled === true)
53
+ disableComponent();
67
54
  return Object.assign(component, {
68
55
  disabled: {
69
56
  enable() {
@@ -133,8 +133,18 @@ export declare const TEXTFIELD_CLASSES: {
133
133
  readonly PREFIX_TEXT: "textfield__prefix";
134
134
  /** Suffix text class */
135
135
  readonly SUFFIX_TEXT: "textfield__suffix";
136
- /** Reserved slot; the current outline is drawn on the root element. */
136
+ /** Outline of the outlined variant, drawn in three segments */
137
137
  readonly OUTLINE: "textfield__outline";
138
+ /** Outline segment before the label (start corner) */
139
+ readonly OUTLINE_LEADING: "textfield__outline-leading";
140
+ /** Outline segment the floating label sits in */
141
+ readonly OUTLINE_NOTCH: "textfield__outline-notch";
142
+ /** Outline segment after the label */
143
+ readonly OUTLINE_TRAILING: "textfield__outline-trailing";
144
+ /** Outline whose notch is open around the floating label */
145
+ readonly OUTLINE_NOTCHED: "textfield__outline--notched";
146
+ /** Outlined field laid out right to left, from its computed direction */
147
+ readonly RTL: "textfield--rtl";
138
148
  /** Multiline class */
139
149
  readonly MULTILINE: "textfield--multiline";
140
150
  };
@@ -59,5 +59,10 @@ export const TEXTFIELD_CLASSES = {
59
59
  PREFIX_TEXT: "textfield__prefix",
60
60
  SUFFIX_TEXT: "textfield__suffix",
61
61
  OUTLINE: "textfield__outline",
62
+ OUTLINE_LEADING: "textfield__outline-leading",
63
+ OUTLINE_NOTCH: "textfield__outline-notch",
64
+ OUTLINE_TRAILING: "textfield__outline-trailing",
65
+ OUTLINE_NOTCHED: "textfield__outline--notched",
66
+ RTL: "textfield--rtl",
62
67
  MULTILINE: "textfield--multiline",
63
68
  };